diff --git a/pkcs7/common.go b/pkcs7/common.go index 546d8e5..588d7b5 100644 --- a/pkcs7/common.go +++ b/pkcs7/common.go @@ -25,57 +25,60 @@ var ( oidPKCS1RSAEncryption = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} ) -type AlgorithmIdentifier struct { +type algorithmIdentifier struct { Algorithm asn1.ObjectIdentifier Parameters asn1.RawValue } -type IssuerAndSerialNumber struct { +type issuerAndSerialNumber struct { Issuer asn1.RawValue SerialNumber *big.Int } -type SignerInfo struct { +type signerInfo struct { Version int - SignedIdentifier IssuerAndSerialNumber - DigestAlgorithm AlgorithmIdentifier - AuthenticatedAttributes Attributes `asn1:"tag:0"` - DigestEncryptionAlgorithm AlgorithmIdentifier + SignedIdentifier issuerAndSerialNumber + DigestAlgorithm algorithmIdentifier + AuthenticatedAttributes []attribute `asn1:"tag:0"` + DigestEncryptionAlgorithm algorithmIdentifier EncryptedDigest []byte UnauthenticatedAttributes int `asn1:"optional"` } -type ContentInfo struct { +type contentInfo struct { ContentType asn1.ObjectIdentifier Content asn1.RawValue `asn1:"optional"` } -type SignedData struct { +type signedData struct { Version int - DigestAlgorithms []AlgorithmIdentifier `asn1:"set"` - ContentInfo ContentInfo + DigestAlgorithms []algorithmIdentifier `asn1:"set"` + ContentInfo contentInfo Certificates asn1.RawValue `asn1:"optional"` Crls asn1.RawValue `asn1:"optional"` - SignerInfos []SignerInfo `asn1:"set"` + SignerInfos []signerInfo `asn1:"set"` } -type SignedDataWrapper struct { +type signedDataWrapper struct { Oid asn1.ObjectIdentifier SignedData asn1.RawValue } // -type Attribute struct { +type attribute struct { Type asn1.ObjectIdentifier Values []interface{} `asn1:"set"` } -type Attributes []Attribute - -func NewAttribute(typ asn1.ObjectIdentifier, val interface{}) Attribute { +func newAttribute(typ asn1.ObjectIdentifier, val interface{}) attribute { if t, ok := val.(time.Time); ok { val = asn1.RawValue{Tag: 23, Bytes: []byte(t.Format("060102150405Z"))} } - return Attribute{Type: typ, Values: []interface{}{val}} + return attribute{ + Type: typ, + Values: []interface{}{ + val, + }, + } } diff --git a/pkcs7/hashable.go b/pkcs7/hashable.go new file mode 100644 index 0000000..748cd94 --- /dev/null +++ b/pkcs7/hashable.go @@ -0,0 +1,44 @@ +package pkcs7 + +import ( + "crypto/sha256" + "io" +) + +type Hashable interface { + Sha256() ([]byte, error) +} + +type hashableReader struct { + r io.Reader +} + +func (h *hashableReader) Sha256() ([]byte, error) { + hash := sha256.New() + + if _, err := io.Copy(hash, h.r); err != nil { + return nil, err + } + + return hash.Sum(nil), nil +} + +// Creates a new Hashable from io.Reader +func NewHashableReader(r io.Reader) Hashable { + return &hashableReader{r: r} +} + +type hashableBytes struct { + b []byte +} + +func (h *hashableBytes) Sha256() ([]byte, error) { + hash := sha256.New() + hash.Write(h.b) + return hash.Sum(nil), nil +} + +// Creates a new Hashable from bytes +func NewHashableBytes(b []byte) Hashable { + return &hashableBytes{b: b} +} diff --git a/pkcs7/hashable_test.go b/pkcs7/hashable_test.go new file mode 100644 index 0000000..43c96ca --- /dev/null +++ b/pkcs7/hashable_test.go @@ -0,0 +1,105 @@ +package pkcs7 + +import ( + "bytes" + "os" + "testing" +) + +func Test_NewHashableReader(t *testing.T) { + cases := []struct { + FileName string + Hash []byte + Error string + }{ + { + FileName: "testdata/test1.txt", + Hash: []byte{ + 112, 80, 230, 94, 235, 41, 94, 59, + 18, 172, 134, 98, 53, 154, 178, 111, + 36, 89, 195, 198, 55, 200, 95, 36, + 193, 157, 229, 137, 123, 224, 99, 221, + }, + Error: "", + }, + } + + for _, c := range cases { + f, err := os.Open(c.FileName) + if err != nil { + t.Errorf("%v", err.Error()) + continue + } + defer f.Close() + + hashable := NewHashableReader(f) + if hashable == nil { + t.Errorf("Got nil from NewHashableReader(\"%v\")") + continue + } + + hash, err := hashable.Sha256() + if c.Error != "" { + if err != nil && err.Error() == c.Error { + continue + } + + t.Errorf("Expected Error %v, found %v", c.Error, err) + continue + } + + if l := len(hash); l != 32 { + t.Errorf("len(hash) must be 32, found %v", l) + continue + } + + if !bytes.Equal(hash, c.Hash) { + t.Errorf("Expected hash %v for file %v, found %v", c.Hash, c.FileName, hash) + } + } +} + +func Test_NewHashableBytes(t *testing.T) { + cases := []struct { + Data []byte + Hash []byte + Error string + }{ + { + Data: []byte("Hello World!"), + Hash: []byte{ + 127, 131, 177, 101, 127, 241, 252, 83, 185, 45, + 193, 129, 72, 161, 214, 93, 252, 45, 75, 31, 163, + 214, 119, 40, 74, 221, 210, 0, 18, 109, 144, 105, + }, + Error: "", + }, + } + + for i, c := range cases { + hashable := NewHashableBytes(c.Data) + if hashable == nil { + t.Errorf("Got nil from NewHashableReader(\"%v\")") + continue + } + + hash, err := hashable.Sha256() + if c.Error != "" { + if err != nil && err.Error() == c.Error { + continue + } + + t.Errorf("Expected Error %v, found %v", c.Error, err) + continue + } + + if l := len(hash); l != 32 { + t.Errorf("len(hash) must be 32, found %v", l) + continue + } + + if !bytes.Equal(hash, c.Hash) { + t.Errorf("Expected hash %v for test %v, found %v", c.Hash, i, hash) + } + } +} diff --git a/pkcs7/sign.go b/pkcs7/sign.go index e406193..f3cce66 100644 --- a/pkcs7/sign.go +++ b/pkcs7/sign.go @@ -8,43 +8,103 @@ import ( "crypto" "crypto/rand" "crypto/rsa" - "crypto/sha256" "crypto/x509" "encoding/asn1" + "errors" "io" "time" ) -func Sign(r io.Reader, cert *x509.Certificate, priv *rsa.PrivateKey) ([]byte, error) { - hash := sha256.New() - if _, err := io.Copy(hash, r); err != nil { +// Create a signature from io.Reader +// Returns the signature and any error encountered. +func Sign(reader io.Reader, certificate *x509.Certificate, privateKey *rsa.PrivateKey) ([]byte, error) { + hashable := NewHashableReader(reader) + return SignDataIntermediate(hashable, certificate, privateKey, nil) +} + +// Create a signature from io.Reader including intermediate certificates. +// Returns the signature and any error encountered. +func SignIntermediate(reader io.Reader, certificate *x509.Certificate, privateKey *rsa.PrivateKey, intermediateCertificates []*x509.Certificate) ([]byte, error) { + hashable := NewHashableReader(reader) + return SignDataIntermediate(hashable, certificate, privateKey, intermediateCertificates) +} + +// Creates a signature +// Returns the signature and any error encountered. +func SignData(hashable Hashable, certificate *x509.Certificate, privateKey *rsa.PrivateKey) ([]byte, error) { + return SignDataIntermediate(hashable, certificate, privateKey, nil) +} + +// Create a signature including intermediate certificates. +// Returns the signature and any error encountered. +func SignDataIntermediate(hashable Hashable, certificate *x509.Certificate, privateKey *rsa.PrivateKey, intermediateCertificates []*x509.Certificate) ([]byte, error) { + // Check if parameters are valid + if certificate == nil { + return nil, errors.New("\"certificate\" cannot be nil.") + } + + if privateKey == nil { + return nil, errors.New("\"privateKey\" cannot be nil.") + } + + messageDigest, err := hashable.Sha256() + if err != nil { return nil, err } - messageDigest := hash.Sum(nil) - signedData := SignedData{ + // Copy intermediateCertificates to certificate stack + raw := certificate.Raw + for _, intermediate := range intermediateCertificates { + if intermediate != nil { + raw = append(raw, intermediate.Raw...) + } + } + + signedData := signedData{ Version: 1, - DigestAlgorithms: []AlgorithmIdentifier{ - AlgorithmIdentifier{Algorithm: oidSHA256, Parameters: asn1.RawValue{Tag: 5}}, + DigestAlgorithms: []algorithmIdentifier{ + { + Algorithm: oidSHA256, + Parameters: asn1.RawValue{ + Tag: 5, + }, + }, }, - ContentInfo: ContentInfo{ + ContentInfo: contentInfo{ ContentType: oidPKCS7Data, }, - Certificates: asn1.RawValue{Class: 2, Tag: 0, Bytes: cert.Raw, IsCompound: true}, - SignerInfos: []SignerInfo{ - SignerInfo{ + Certificates: asn1.RawValue{ + Class: 2, + Tag: 0, + Bytes: raw, + IsCompound: true, + }, + SignerInfos: []signerInfo{ + { Version: 1, - SignedIdentifier: IssuerAndSerialNumber{ - Issuer: asn1.RawValue{FullBytes: cert.RawIssuer}, - SerialNumber: cert.SerialNumber, + SignedIdentifier: issuerAndSerialNumber{ + Issuer: asn1.RawValue{ + FullBytes: certificate.RawIssuer, + }, + SerialNumber: certificate.SerialNumber, + }, + DigestAlgorithm: algorithmIdentifier{ + Algorithm: oidSHA256, + Parameters: asn1.RawValue{ + Tag: 5, + }, }, - DigestAlgorithm: AlgorithmIdentifier{Algorithm: oidSHA256, Parameters: asn1.RawValue{Tag: 5}}, - AuthenticatedAttributes: Attributes{ - NewAttribute(oidPKCS9ContentType, oidPKCS7Data), - NewAttribute(oidPKCS9SigningTime, time.Now().UTC()), - NewAttribute(oidPKCS9MessageDigest, messageDigest), + AuthenticatedAttributes: []attribute{ + newAttribute(oidPKCS9ContentType, oidPKCS7Data), + newAttribute(oidPKCS9SigningTime, time.Now().UTC()), + newAttribute(oidPKCS9MessageDigest, messageDigest), + }, + DigestEncryptionAlgorithm: algorithmIdentifier{ + Algorithm: oidPKCS1RSAEncryption, + Parameters: asn1.RawValue{ + Tag: 5, + }, }, - DigestEncryptionAlgorithm: AlgorithmIdentifier{Algorithm: oidPKCS1RSAEncryption, Parameters: asn1.RawValue{Tag: 5}}, EncryptedDigest: nil, // We fill this in later UnauthenticatedAttributes: 0, }, @@ -56,20 +116,18 @@ func Sign(r io.Reader, cert *x509.Certificate, priv *rsa.PrivateKey) ([]byte, er return nil, err } - // For the digest of the authenticated attributes, we need a - // slightly different encoding. Change the attributes from a - // SEQUENCE to a SET. - originalFirstByte := encodedAuthenticatedAttributes[0] encodedAuthenticatedAttributes[0] = 0x31 - hash = sha256.New() - hash.Write(encodedAuthenticatedAttributes) - attributesDigest := hash.Sum(nil) + digest := NewHashableBytes(encodedAuthenticatedAttributes) + attributesDigest, err := digest.Sha256() + if err != nil { + return nil, err + } encodedAuthenticatedAttributes[0] = originalFirstByte - encryptedDigest, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA256, attributesDigest) + encryptedDigest, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, attributesDigest) if err != nil { return nil, err } @@ -80,7 +138,7 @@ func Sign(r io.Reader, cert *x509.Certificate, priv *rsa.PrivateKey) ([]byte, er return nil, err } - signedDataWrapper := SignedDataWrapper{ + signedDataWrapper := signedDataWrapper{ Oid: oidPKCS7SignedData, SignedData: asn1.RawValue{Class: 2, Tag: 0, Bytes: encodedSignedData, IsCompound: true}, } diff --git a/pkcs7/sign_test.go b/pkcs7/sign_test.go index 559e2c9..8fa8dcc 100644 --- a/pkcs7/sign_test.go +++ b/pkcs7/sign_test.go @@ -14,9 +14,9 @@ import ( // // Test key and certificate are in testdata/ and were generated with OpenSSL as follows: // -// openssl genrsa -out test.key 2048 -// openssl req -nodes -new -key test.key -out test.csr -subj "/C=CA/ST=Ontario/L=Toronto/O=Stefan Arentz/OU=Golang Hacks/CN=example.com" -// openssl x509 -req -days 1825 -in test.csr -signkey test.key -out test.crt +// openssl genrsa -out test1.key 2048 +// openssl req -nodes -new -key test1.key -out test1.csr -subj "/C=CA/ST=Ontario/L=Toronto/O=Stefan Arentz/OU=Golang Hacks/CN=example.com" +// openssl x509 -req -days 1825 -in test1.csr -signkey test1.key -out test1.crt // func loadPKCS1PrivateKey(path string) (*rsa.PrivateKey, error) { @@ -49,44 +49,77 @@ func loadCertificate(path string) (*x509.Certificate, error) { return x509.ParseCertificate(block.Bytes) } -func Test_Sign(t *testing.T) { - key, err := loadPKCS1PrivateKey("testdata/test.key") - if err != nil { - panic(err) +func verifySignature(file string, signature []byte) error { + if err := ioutil.WriteFile("/tmp/signature", signature, 0600); err != nil { + return err } - cert, err := loadCertificate("testdata/test.crt") - if err != nil { - panic(err) + cmd := exec.Command("openssl", "smime", "-verify", "-in", "/tmp/signature", "-content", file, "-inform", "der", "-noverify") + if err := cmd.Start(); err != nil { + return err } - f, err := os.Open("testdata/test.txt") - if err != nil { - panic(err) + if err := cmd.Wait(); err != nil { + return err } - defer f.Close() - signature, err := Sign(f, cert, key) - if err != nil { - t.Error("Cannot Sign:", err) - } + return nil +} - if len(signature) == 0 { - t.Error("Signature is zero length") +func Test_Sign(t *testing.T) { + cases := []struct { + FileName string + Certificate string + PrivateKey string + Error string + }{ + { + FileName: "testdata/test1.txt", + Certificate: "testdata/test1.crt", + PrivateKey: "testdata/test1.key", + Error: "", + }, } - // Verify the signature with OpenSSL + for _, c := range cases { + cert, err := loadCertificate(c.Certificate) + if err != nil { + t.Errorf("%v", err.Error()) + continue + } - if err := ioutil.WriteFile("/tmp/signature", signature, 0600); err != nil { - panic(err) - } + key, err := loadPKCS1PrivateKey(c.PrivateKey) + if err != nil { + t.Errorf("%v", err.Error()) + continue + } - cmd := exec.Command("openssl", "smime", "-verify", "-in", "/tmp/signature", "-content", "testdata/test.txt", "-inform", "der", "-noverify") - if err := cmd.Start(); err != nil { - t.Error("Failed to start openssl:", err) - } + f, err := os.Open(c.FileName) + if err != nil { + t.Errorf("%v", err.Error()) + continue + } + defer f.Close() - if err := cmd.Wait(); err != nil { - t.Error("Failed to verify signature with openssl:", err) + signature, err := Sign(f, cert, key) + + if c.Error != "" { + // Sign should fail + if err != nil && err.Error() == c.Error { + continue + } + + t.Errorf("Expected Error %v, found %v", c.Error, err) + continue + } + + if len(signature) <= 0 { + t.Errorf("Signature is zero length") + continue + } + + if err := verifySignature(c.FileName, signature); err != nil { + t.Errorf("Failed to verify signature with openssl: %v", err.Error()) + } } } diff --git a/pkcs7/testdata/test.crt b/pkcs7/testdata/test1.crt similarity index 100% rename from pkcs7/testdata/test.crt rename to pkcs7/testdata/test1.crt diff --git a/pkcs7/testdata/test.csr b/pkcs7/testdata/test1.csr similarity index 100% rename from pkcs7/testdata/test.csr rename to pkcs7/testdata/test1.csr diff --git a/pkcs7/testdata/test.key b/pkcs7/testdata/test1.key similarity index 100% rename from pkcs7/testdata/test.key rename to pkcs7/testdata/test1.key diff --git a/pkcs7/testdata/test.txt b/pkcs7/testdata/test1.txt similarity index 100% rename from pkcs7/testdata/test.txt rename to pkcs7/testdata/test1.txt