openpgp: support creating signatures compatible with 'gpg --sign'.

This is neither a '--clearsign' nor a '--detach-sign' which are already
supported.  Verification of these signatures is already supported by
ReadMessage.

The code shares a lot with standard encrypt/sign, so mostly a
refactoring of 'Encrypt' to allow use of the code path without
actually doing a signing.

Change-Id: I5bb7487134ffcf1189ed74e28dbbbe1c01b356d1
GitHub-Last-Rev: 01162222600a4a2a230a444387b33883faf596ec
GitHub-Pull-Request: golang/crypto#50
Reviewed-on: https://go-review.googlesource.com/116017
Reviewed-by: Filippo Valsorda <filippo@golang.org>
diff --git a/openpgp/write.go b/openpgp/write.go
index 65a304c..d6dede7 100644
--- a/openpgp/write.go
+++ b/openpgp/write.go
@@ -164,12 +164,12 @@
 	return v
 }
 
-// Encrypt encrypts a message to a number of recipients and, optionally, signs
-// it. hints contains optional information, that is also encrypted, that aids
-// the recipients in processing the message. The resulting WriteCloser must
-// be closed after the contents of the file have been written.
-// If config is nil, sensible defaults will be used.
-func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
+// writeAndSign writes the data as a payload package and, optionally, signs
+// it. hints contains optional information, that is also encrypted,
+// that aids the recipients in processing the message. The resulting
+// WriteCloser must be closed after the contents of the file have been
+// written. If config is nil, sensible defaults will be used.
+func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
 	var signer *packet.PrivateKey
 	if signed != nil {
 		signKey, ok := signed.signingKey(config.Now())
@@ -185,6 +185,83 @@
 		}
 	}
 
+	var hash crypto.Hash
+	for _, hashId := range candidateHashes {
+		if h, ok := s2k.HashIdToHash(hashId); ok && h.Available() {
+			hash = h
+			break
+		}
+	}
+
+	// If the hash specified by config is a candidate, we'll use that.
+	if configuredHash := config.Hash(); configuredHash.Available() {
+		for _, hashId := range candidateHashes {
+			if h, ok := s2k.HashIdToHash(hashId); ok && h == configuredHash {
+				hash = h
+				break
+			}
+		}
+	}
+
+	if hash == 0 {
+		hashId := candidateHashes[0]
+		name, ok := s2k.HashIdToString(hashId)
+		if !ok {
+			name = "#" + strconv.Itoa(int(hashId))
+		}
+		return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
+	}
+
+	if signer != nil {
+		ops := &packet.OnePassSignature{
+			SigType:    packet.SigTypeBinary,
+			Hash:       hash,
+			PubKeyAlgo: signer.PubKeyAlgo,
+			KeyId:      signer.KeyId,
+			IsLast:     true,
+		}
+		if err := ops.Serialize(payload); err != nil {
+			return nil, err
+		}
+	}
+
+	if hints == nil {
+		hints = &FileHints{}
+	}
+
+	w := payload
+	if signer != nil {
+		// If we need to write a signature packet after the literal
+		// data then we need to stop literalData from closing
+		// encryptedData.
+		w = noOpCloser{w}
+
+	}
+	var epochSeconds uint32
+	if !hints.ModTime.IsZero() {
+		epochSeconds = uint32(hints.ModTime.Unix())
+	}
+	literalData, err := packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds)
+	if err != nil {
+		return nil, err
+	}
+
+	if signer != nil {
+		return signatureWriter{payload, literalData, hash, hash.New(), signer, config}, nil
+	}
+	return literalData, nil
+}
+
+// Encrypt encrypts a message to a number of recipients and, optionally, signs
+// it. hints contains optional information, that is also encrypted, that aids
+// the recipients in processing the message. The resulting WriteCloser must
+// be closed after the contents of the file have been written.
+// If config is nil, sensible defaults will be used.
+func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
+	if len(to) == 0 {
+		return nil, errors.InvalidArgumentError("no encryption recipient provided")
+	}
+
 	// These are the possible ciphers that we'll use for the message.
 	candidateCiphers := []uint8{
 		uint8(packet.CipherAES128),
@@ -241,33 +318,6 @@
 		}
 	}
 
-	var hash crypto.Hash
-	for _, hashId := range candidateHashes {
-		if h, ok := s2k.HashIdToHash(hashId); ok && h.Available() {
-			hash = h
-			break
-		}
-	}
-
-	// If the hash specified by config is a candidate, we'll use that.
-	if configuredHash := config.Hash(); configuredHash.Available() {
-		for _, hashId := range candidateHashes {
-			if h, ok := s2k.HashIdToHash(hashId); ok && h == configuredHash {
-				hash = h
-				break
-			}
-		}
-	}
-
-	if hash == 0 {
-		hashId := candidateHashes[0]
-		name, ok := s2k.HashIdToString(hashId)
-		if !ok {
-			name = "#" + strconv.Itoa(int(hashId))
-		}
-		return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
-	}
-
 	symKey := make([]byte, cipher.KeySize())
 	if _, err := io.ReadFull(config.Random(), symKey); err != nil {
 		return nil, err
@@ -279,49 +329,37 @@
 		}
 	}
 
-	encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, cipher, symKey, config)
+	payload, err := packet.SerializeSymmetricallyEncrypted(ciphertext, cipher, symKey, config)
 	if err != nil {
 		return
 	}
 
-	if signer != nil {
-		ops := &packet.OnePassSignature{
-			SigType:    packet.SigTypeBinary,
-			Hash:       hash,
-			PubKeyAlgo: signer.PubKeyAlgo,
-			KeyId:      signer.KeyId,
-			IsLast:     true,
-		}
-		if err := ops.Serialize(encryptedData); err != nil {
-			return nil, err
-		}
+	return writeAndSign(payload, candidateHashes, signed, hints, config)
+}
+
+// Sign signs a message. The resulting WriteCloser must be closed after the
+// contents of the file have been written.  hints contains optional information
+// that aids the recipients in processing the message.
+// If config is nil, sensible defaults will be used.
+func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Config) (input io.WriteCloser, err error) {
+	if signed == nil {
+		return nil, errors.InvalidArgumentError("no signer provided")
 	}
 
-	if hints == nil {
-		hints = &FileHints{}
+	// These are the possible hash functions that we'll use for the signature.
+	candidateHashes := []uint8{
+		hashToHashId(crypto.SHA256),
+		hashToHashId(crypto.SHA512),
+		hashToHashId(crypto.SHA1),
+		hashToHashId(crypto.RIPEMD160),
 	}
-
-	w := encryptedData
-	if signer != nil {
-		// If we need to write a signature packet after the literal
-		// data then we need to stop literalData from closing
-		// encryptedData.
-		w = noOpCloser{encryptedData}
-
+	defaultHashes := candidateHashes[len(candidateHashes)-1:]
+	preferredHashes := signed.primaryIdentity().SelfSignature.PreferredHash
+	if len(preferredHashes) == 0 {
+		preferredHashes = defaultHashes
 	}
-	var epochSeconds uint32
-	if !hints.ModTime.IsZero() {
-		epochSeconds = uint32(hints.ModTime.Unix())
-	}
-	literalData, err := packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds)
-	if err != nil {
-		return nil, err
-	}
-
-	if signer != nil {
-		return signatureWriter{encryptedData, literalData, hash, hash.New(), signer, config}, nil
-	}
-	return literalData, nil
+	candidateHashes = intersectPreferences(candidateHashes, preferredHashes)
+	return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, config)
 }
 
 // signatureWriter hashes the contents of a message while passing it along to
diff --git a/openpgp/write_test.go b/openpgp/write_test.go
index f2d50a0..cbc8f4d 100644
--- a/openpgp/write_test.go
+++ b/openpgp/write_test.go
@@ -271,3 +271,92 @@
 		}
 	}
 }
+
+var testSigningTests = []struct {
+	keyRingHex string
+}{
+	{
+		testKeys1And2PrivateHex,
+	},
+	{
+		dsaElGamalTestKeysHex,
+	},
+}
+
+func TestSigning(t *testing.T) {
+	for i, test := range testSigningTests {
+		kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex))
+
+		passphrase := []byte("passphrase")
+		for _, entity := range kring {
+			if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
+				err := entity.PrivateKey.Decrypt(passphrase)
+				if err != nil {
+					t.Errorf("#%d: failed to decrypt key", i)
+				}
+			}
+			for _, subkey := range entity.Subkeys {
+				if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
+					err := subkey.PrivateKey.Decrypt(passphrase)
+					if err != nil {
+						t.Errorf("#%d: failed to decrypt subkey", i)
+					}
+				}
+			}
+		}
+
+		signed := kring[0]
+
+		buf := new(bytes.Buffer)
+		w, err := Sign(buf, signed, nil /* no hints */, nil)
+		if err != nil {
+			t.Errorf("#%d: error in Sign: %s", i, err)
+			continue
+		}
+
+		const message = "testing"
+		_, err = w.Write([]byte(message))
+		if err != nil {
+			t.Errorf("#%d: error writing plaintext: %s", i, err)
+			continue
+		}
+		err = w.Close()
+		if err != nil {
+			t.Errorf("#%d: error closing WriteCloser: %s", i, err)
+			continue
+		}
+
+		md, err := ReadMessage(buf, kring, nil /* no prompt */, nil)
+		if err != nil {
+			t.Errorf("#%d: error reading message: %s", i, err)
+			continue
+		}
+
+		testTime, _ := time.Parse("2006-01-02", "2013-07-01")
+		signKey, _ := kring[0].signingKey(testTime)
+		expectedKeyId := signKey.PublicKey.KeyId
+		if md.SignedByKeyId != expectedKeyId {
+			t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignedBy, expectedKeyId)
+		}
+		if md.SignedBy == nil {
+			t.Errorf("#%d: failed to find the signing Entity", i)
+		}
+
+		plaintext, err := ioutil.ReadAll(md.UnverifiedBody)
+		if err != nil {
+			t.Errorf("#%d: error reading contents: %v", i, err)
+			continue
+		}
+
+		if string(plaintext) != message {
+			t.Errorf("#%d: got: %q, want: %q", i, plaintext, message)
+		}
+
+		if md.SignatureError != nil {
+			t.Errorf("#%d: signature error: %q", i, md.SignatureError)
+		}
+		if md.Signature == nil {
+			t.Error("signature missing")
+		}
+	}
+}