From bf12d97ef899b538e83ef7d4cb07fd049d3a2749 Mon Sep 17 00:00:00 2001 From: Max Altgelt Date: Fri, 24 Nov 2023 13:46:39 +0100 Subject: [PATCH 1/3] feat: support copying PKCS7 structs --- sign.go | 26 +++++++++++++++++++++ sign_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ verify.go | 13 +++++++++++ 3 files changed, 103 insertions(+) diff --git a/sign.go b/sign.go index 31c3654..eb07f30 100644 --- a/sign.go +++ b/sign.go @@ -23,6 +23,32 @@ type SignedData struct { encryptionOid asn1.ObjectIdentifier } +// Copy creates a new SignedData struct based on existing PKCS7 signed data. +func Copy(data *PKCS7, unauthenticatedAttrs ...Attribute) ([]byte, error) { + rawData, isSignedData := data.raw.(signedData) + if !isSignedData { + return nil, ErrUnsupportedContentType + } + + // Make a copy of the signer infos since we change them + rawData.SignerInfos = append([]signerInfo{}, rawData.SignerInfos...) + for i := range rawData.SignerInfos { + err := rawData.SignerInfos[i].SetUnauthenticatedAttributes(unauthenticatedAttrs) + if err != nil { + return nil, err + } + } + inner, err := asn1.Marshal(rawData) + 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) +} + // NewSignedData takes data and initializes a PKCS7 SignedData struct that is // ready to be signed via AddSigner. The digest algorithm is set to SHA1 by default // and can be changed by calling SetDigestAlgorithm. diff --git a/sign_test.go b/sign_test.go index 0ba6324..65e544d 100644 --- a/sign_test.go +++ b/sign_test.go @@ -264,3 +264,67 @@ func fromHex(s string) *big.Int { } return result } + +func TestCopy(t *testing.T) { + content := []byte("Hello World") + rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil, x509.SHA256WithRSA, true) + if err != nil { + t.Fatalf("cannot generate root cert: %s", err) + } + truststore := x509.NewCertPool() + truststore.AddCert(rootCert.Certificate) + signerCert, err := createTestCertificateByIssuer("PKCS7 Test Signer Cert", rootCert, x509.SHA256WithRSA, false) + if err != nil { + t.Fatalf("cannot generate signer cert: %s", err) + } + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("cannot initialize signed data: %s", err) + } + + // Set the digest to match the end entity cert + signerDigest, _ := getDigestOIDForSignatureAlgorithm(signerCert.Certificate.SignatureAlgorithm) + toBeSigned.SetDigestAlgorithm(signerDigest) + + if err := toBeSigned.AddSignerChain(signerCert.Certificate, *signerCert.PrivateKey, nil, SignerInfoConfig{}); err != nil { + t.Fatalf("cannot add signer: %s", err) + } + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("cannot finish signing data: %s", err) + } + pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: signed}) + + p7, err := Parse(signed) + if err != nil { + t.Fatalf("cannot parse signed data: %s", err) + } + testOid := asn1.ObjectIdentifier{2, 3, 4, 5, 6, 7} + testValue := "TestValue" + copied, err := Copy(p7, Attribute{ + Type: testOid, + Value: testValue, + }) + if err != nil { + t.Fatalf("cannot copy signed data: %s", err) + } + p7, err = Parse(copied) + if err != nil { + t.Fatalf("cannot parse copied data: %s", err) + } + + if !bytes.Equal(content, p7.Content) { + t.Errorf("content was not found in the copied data:\n\tExpected: %s\n\tActual: %s", content, p7.Content) + } + if err := p7.VerifyWithChain(truststore); err != nil { + t.Errorf("cannot verify copied data: %s", err) + } + + var copiedValue string + if err := p7.UnmarshalUnsignedAttribute(testOid, &copiedValue); err != nil { + t.Fatalf("could not unmarshal attribute: %s", err) + } + if copiedValue != testValue { + t.Errorf("incorrect attribute value: %s", copiedValue) + } +} diff --git a/verify.go b/verify.go index 2c6f32c..4fdef78 100644 --- a/verify.go +++ b/verify.go @@ -189,6 +189,19 @@ func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, o return unmarshalAttribute(attributes, attributeType, out) } +// UnmarshalUnsignedAttribute decodes a single attribute from the signer info +func (p7 *PKCS7) UnmarshalUnsignedAttribute(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].UnauthenticatedAttributes + return unmarshalAttribute(attributes, attributeType, out) +} + func parseSignedData(data []byte) (*PKCS7, error) { var sd signedData asn1.Unmarshal(data, &sd) From 318f32c31045866815b787c547717cf047e6ffd0 Mon Sep 17 00:00:00 2001 From: Max Altgelt Date: Fri, 22 Mar 2024 11:22:15 +0100 Subject: [PATCH 2/3] feat: specific error for not found attribute --- verify.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/verify.go b/verify.go index 4fdef78..e1fed88 100644 --- a/verify.go +++ b/verify.go @@ -350,6 +350,14 @@ func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndS return nil } +type AttributeNotFoundError struct { + AttributeType asn1.ObjectIdentifier +} + +func (a AttributeNotFoundError) Error() string { + return fmt.Sprintf("pkcs7: attribute %s does not exist", a.AttributeType) +} + func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error { for _, attr := range attrs { if attr.Type.Equal(attributeType) { @@ -357,5 +365,5 @@ func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, return err } } - return errors.New("pkcs7: attribute type not in attributes") + return AttributeNotFoundError{AttributeType: attributeType} } From c3bf0ff80ca1ce9dbd962c0713ef1853f05e9857 Mon Sep 17 00:00:00 2001 From: Max Altgelt Date: Fri, 3 May 2024 13:38:06 +0100 Subject: [PATCH 3/3] refactor: split copy / marshal --- marshal.go | 25 +++++++++++++++++++++ marshal_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++ sign.go | 20 +++++++++-------- sign_test.go | 8 ++----- 4 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 marshal.go create mode 100644 marshal_test.go diff --git a/marshal.go b/marshal.go new file mode 100644 index 0000000..e94339b --- /dev/null +++ b/marshal.go @@ -0,0 +1,25 @@ +package pkcs7 + +import "encoding/asn1" + +func (p7 *PKCS7) Marshal() ([]byte, error) { + var contentType asn1.ObjectIdentifier + switch p7.raw.(type) { + case signedData: + contentType = OIDSignedData + case envelopedData: + contentType = OIDEnvelopedData + case encryptedData: + contentType = OIDEncryptedData + default: + return nil, ErrUnsupportedContentType + } + inner, err := asn1.Marshal(p7.raw) + if err != nil { + return nil, err + } + return asn1.Marshal(contentInfo{ + ContentType: contentType, + Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true}, + }) +} diff --git a/marshal_test.go b/marshal_test.go new file mode 100644 index 0000000..204b450 --- /dev/null +++ b/marshal_test.go @@ -0,0 +1,60 @@ +package pkcs7 + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + "os" + "testing" +) + +func TestPKCS7_Marshal(t *testing.T) { + content := []byte("Hello World") + rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil, x509.SHA256WithRSA, true) + if err != nil { + t.Fatalf("cannot generate root cert: %s", err) + } + truststore := x509.NewCertPool() + truststore.AddCert(rootCert.Certificate) + signerCert, err := createTestCertificateByIssuer("PKCS7 Test Signer Cert", rootCert, x509.SHA256WithRSA, false) + if err != nil { + t.Fatalf("cannot generate signer cert: %s", err) + } + toBeSigned, err := NewSignedData(content) + if err != nil { + t.Fatalf("cannot initialize signed data: %s", err) + } + + // Set the digest to match the end entity cert + signerDigest, _ := getDigestOIDForSignatureAlgorithm(signerCert.Certificate.SignatureAlgorithm) + toBeSigned.SetDigestAlgorithm(signerDigest) + + if err := toBeSigned.AddSignerChain(signerCert.Certificate, *signerCert.PrivateKey, nil, SignerInfoConfig{}); err != nil { + t.Fatalf("cannot add signer: %s", err) + } + signed, err := toBeSigned.Finish() + if err != nil { + t.Fatalf("cannot finish signing data: %s", err) + } + pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: signed}) + + p7, err := Parse(signed) + if err != nil { + t.Fatalf("cannot parse signed data: %s", err) + } + + marshaled, err := p7.Marshal() + if err != nil { + t.Fatalf("cannot marshal signed data: %s", err) + } + p7Reparsed, err := Parse(marshaled) + if err != nil { + t.Fatalf("cannot reparse signed data: %s", err) + } + if !bytes.Equal(p7.Content, p7Reparsed.Content) { + t.Errorf("content was not found in the reparsed data:\n\tExpected: %s\n\tActual: %s", p7.Content, p7Reparsed.Content) + } + if err := p7Reparsed.VerifyWithChain(truststore); err != nil { + t.Errorf("cannot verify reparsed data: %s", err) + } +} diff --git a/sign.go b/sign.go index eb07f30..8ecc18d 100644 --- a/sign.go +++ b/sign.go @@ -23,9 +23,9 @@ type SignedData struct { encryptionOid asn1.ObjectIdentifier } -// Copy creates a new SignedData struct based on existing PKCS7 signed data. -func Copy(data *PKCS7, unauthenticatedAttrs ...Attribute) ([]byte, error) { - rawData, isSignedData := data.raw.(signedData) +// CopyWithUnsignedAttributes creates a copy of the PKCS7 struct with different unsigned attributes. +func (p7 *PKCS7) CopyWithUnsignedAttributes(unauthenticatedAttrs ...Attribute) (*PKCS7, error) { + rawData, isSignedData := p7.raw.(signedData) if !isSignedData { return nil, ErrUnsupportedContentType } @@ -38,15 +38,17 @@ func Copy(data *PKCS7, unauthenticatedAttrs ...Attribute) ([]byte, error) { return nil, err } } - inner, err := asn1.Marshal(rawData) + certs, err := rawData.Certificates.Parse() 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) + return &PKCS7{ + Content: p7.Content, + Certificates: certs, + CRLs: rawData.CRLs, + Signers: rawData.SignerInfos, + raw: rawData, + }, nil } // NewSignedData takes data and initializes a PKCS7 SignedData struct that is diff --git a/sign_test.go b/sign_test.go index 65e544d..ee1a2c4 100644 --- a/sign_test.go +++ b/sign_test.go @@ -265,7 +265,7 @@ func fromHex(s string) *big.Int { return result } -func TestCopy(t *testing.T) { +func TestCopyWithUnsignedAttributes(t *testing.T) { content := []byte("Hello World") rootCert, err := createTestCertificateByIssuer("PKCS7 Test Root CA", nil, x509.SHA256WithRSA, true) if err != nil { @@ -301,17 +301,13 @@ func TestCopy(t *testing.T) { } testOid := asn1.ObjectIdentifier{2, 3, 4, 5, 6, 7} testValue := "TestValue" - copied, err := Copy(p7, Attribute{ + p7, err = p7.CopyWithUnsignedAttributes(Attribute{ Type: testOid, Value: testValue, }) if err != nil { t.Fatalf("cannot copy signed data: %s", err) } - p7, err = Parse(copied) - if err != nil { - t.Fatalf("cannot parse copied data: %s", err) - } if !bytes.Equal(content, p7.Content) { t.Errorf("content was not found in the copied data:\n\tExpected: %s\n\tActual: %s", content, p7.Content)