Skip to content

Commit a214413

Browse files
wteikenFiloSottile
authored andcommitted
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: 0116222 GitHub-Pull-Request: golang#50 Reviewed-on: https://go-review.googlesource.com/116017 Reviewed-by: Filippo Valsorda <[email protected]>
1 parent a49355c commit a214413

File tree

2 files changed

+194
-67
lines changed

2 files changed

+194
-67
lines changed

openpgp/write.go

Lines changed: 105 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -164,12 +164,12 @@ func hashToHashId(h crypto.Hash) uint8 {
164164
return v
165165
}
166166

167-
// Encrypt encrypts a message to a number of recipients and, optionally, signs
168-
// it. hints contains optional information, that is also encrypted, that aids
169-
// the recipients in processing the message. The resulting WriteCloser must
170-
// be closed after the contents of the file have been written.
171-
// If config is nil, sensible defaults will be used.
172-
func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
167+
// writeAndSign writes the data as a payload package and, optionally, signs
168+
// it. hints contains optional information, that is also encrypted,
169+
// that aids the recipients in processing the message. The resulting
170+
// WriteCloser must be closed after the contents of the file have been
171+
// written. If config is nil, sensible defaults will be used.
172+
func writeAndSign(payload io.WriteCloser, candidateHashes []uint8, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
173173
var signer *packet.PrivateKey
174174
if signed != nil {
175175
signKey, ok := signed.signingKey(config.Now())
@@ -185,6 +185,83 @@ func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHint
185185
}
186186
}
187187

188+
var hash crypto.Hash
189+
for _, hashId := range candidateHashes {
190+
if h, ok := s2k.HashIdToHash(hashId); ok && h.Available() {
191+
hash = h
192+
break
193+
}
194+
}
195+
196+
// If the hash specified by config is a candidate, we'll use that.
197+
if configuredHash := config.Hash(); configuredHash.Available() {
198+
for _, hashId := range candidateHashes {
199+
if h, ok := s2k.HashIdToHash(hashId); ok && h == configuredHash {
200+
hash = h
201+
break
202+
}
203+
}
204+
}
205+
206+
if hash == 0 {
207+
hashId := candidateHashes[0]
208+
name, ok := s2k.HashIdToString(hashId)
209+
if !ok {
210+
name = "#" + strconv.Itoa(int(hashId))
211+
}
212+
return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
213+
}
214+
215+
if signer != nil {
216+
ops := &packet.OnePassSignature{
217+
SigType: packet.SigTypeBinary,
218+
Hash: hash,
219+
PubKeyAlgo: signer.PubKeyAlgo,
220+
KeyId: signer.KeyId,
221+
IsLast: true,
222+
}
223+
if err := ops.Serialize(payload); err != nil {
224+
return nil, err
225+
}
226+
}
227+
228+
if hints == nil {
229+
hints = &FileHints{}
230+
}
231+
232+
w := payload
233+
if signer != nil {
234+
// If we need to write a signature packet after the literal
235+
// data then we need to stop literalData from closing
236+
// encryptedData.
237+
w = noOpCloser{w}
238+
239+
}
240+
var epochSeconds uint32
241+
if !hints.ModTime.IsZero() {
242+
epochSeconds = uint32(hints.ModTime.Unix())
243+
}
244+
literalData, err := packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds)
245+
if err != nil {
246+
return nil, err
247+
}
248+
249+
if signer != nil {
250+
return signatureWriter{payload, literalData, hash, hash.New(), signer, config}, nil
251+
}
252+
return literalData, nil
253+
}
254+
255+
// Encrypt encrypts a message to a number of recipients and, optionally, signs
256+
// it. hints contains optional information, that is also encrypted, that aids
257+
// the recipients in processing the message. The resulting WriteCloser must
258+
// be closed after the contents of the file have been written.
259+
// If config is nil, sensible defaults will be used.
260+
func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHints, config *packet.Config) (plaintext io.WriteCloser, err error) {
261+
if len(to) == 0 {
262+
return nil, errors.InvalidArgumentError("no encryption recipient provided")
263+
}
264+
188265
// These are the possible ciphers that we'll use for the message.
189266
candidateCiphers := []uint8{
190267
uint8(packet.CipherAES128),
@@ -241,33 +318,6 @@ func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHint
241318
}
242319
}
243320

244-
var hash crypto.Hash
245-
for _, hashId := range candidateHashes {
246-
if h, ok := s2k.HashIdToHash(hashId); ok && h.Available() {
247-
hash = h
248-
break
249-
}
250-
}
251-
252-
// If the hash specified by config is a candidate, we'll use that.
253-
if configuredHash := config.Hash(); configuredHash.Available() {
254-
for _, hashId := range candidateHashes {
255-
if h, ok := s2k.HashIdToHash(hashId); ok && h == configuredHash {
256-
hash = h
257-
break
258-
}
259-
}
260-
}
261-
262-
if hash == 0 {
263-
hashId := candidateHashes[0]
264-
name, ok := s2k.HashIdToString(hashId)
265-
if !ok {
266-
name = "#" + strconv.Itoa(int(hashId))
267-
}
268-
return nil, errors.InvalidArgumentError("cannot encrypt because no candidate hash functions are compiled in. (Wanted " + name + " in this case.)")
269-
}
270-
271321
symKey := make([]byte, cipher.KeySize())
272322
if _, err := io.ReadFull(config.Random(), symKey); err != nil {
273323
return nil, err
@@ -279,49 +329,37 @@ func Encrypt(ciphertext io.Writer, to []*Entity, signed *Entity, hints *FileHint
279329
}
280330
}
281331

282-
encryptedData, err := packet.SerializeSymmetricallyEncrypted(ciphertext, cipher, symKey, config)
332+
payload, err := packet.SerializeSymmetricallyEncrypted(ciphertext, cipher, symKey, config)
283333
if err != nil {
284334
return
285335
}
286336

287-
if signer != nil {
288-
ops := &packet.OnePassSignature{
289-
SigType: packet.SigTypeBinary,
290-
Hash: hash,
291-
PubKeyAlgo: signer.PubKeyAlgo,
292-
KeyId: signer.KeyId,
293-
IsLast: true,
294-
}
295-
if err := ops.Serialize(encryptedData); err != nil {
296-
return nil, err
297-
}
298-
}
337+
return writeAndSign(payload, candidateHashes, signed, hints, config)
338+
}
299339

300-
if hints == nil {
301-
hints = &FileHints{}
340+
// Sign signs a message. The resulting WriteCloser must be closed after the
341+
// contents of the file have been written. hints contains optional information
342+
// that aids the recipients in processing the message.
343+
// If config is nil, sensible defaults will be used.
344+
func Sign(output io.Writer, signed *Entity, hints *FileHints, config *packet.Config) (input io.WriteCloser, err error) {
345+
if signed == nil {
346+
return nil, errors.InvalidArgumentError("no signer provided")
302347
}
303348

304-
w := encryptedData
305-
if signer != nil {
306-
// If we need to write a signature packet after the literal
307-
// data then we need to stop literalData from closing
308-
// encryptedData.
309-
w = noOpCloser{encryptedData}
310-
311-
}
312-
var epochSeconds uint32
313-
if !hints.ModTime.IsZero() {
314-
epochSeconds = uint32(hints.ModTime.Unix())
315-
}
316-
literalData, err := packet.SerializeLiteral(w, hints.IsBinary, hints.FileName, epochSeconds)
317-
if err != nil {
318-
return nil, err
349+
// These are the possible hash functions that we'll use for the signature.
350+
candidateHashes := []uint8{
351+
hashToHashId(crypto.SHA256),
352+
hashToHashId(crypto.SHA512),
353+
hashToHashId(crypto.SHA1),
354+
hashToHashId(crypto.RIPEMD160),
319355
}
320-
321-
if signer != nil {
322-
return signatureWriter{encryptedData, literalData, hash, hash.New(), signer, config}, nil
356+
defaultHashes := candidateHashes[len(candidateHashes)-1:]
357+
preferredHashes := signed.primaryIdentity().SelfSignature.PreferredHash
358+
if len(preferredHashes) == 0 {
359+
preferredHashes = defaultHashes
323360
}
324-
return literalData, nil
361+
candidateHashes = intersectPreferences(candidateHashes, preferredHashes)
362+
return writeAndSign(noOpCloser{output}, candidateHashes, signed, hints, config)
325363
}
326364

327365
// signatureWriter hashes the contents of a message while passing it along to

openpgp/write_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,92 @@ func TestEncryption(t *testing.T) {
271271
}
272272
}
273273
}
274+
275+
var testSigningTests = []struct {
276+
keyRingHex string
277+
}{
278+
{
279+
testKeys1And2PrivateHex,
280+
},
281+
{
282+
dsaElGamalTestKeysHex,
283+
},
284+
}
285+
286+
func TestSigning(t *testing.T) {
287+
for i, test := range testSigningTests {
288+
kring, _ := ReadKeyRing(readerFromHex(test.keyRingHex))
289+
290+
passphrase := []byte("passphrase")
291+
for _, entity := range kring {
292+
if entity.PrivateKey != nil && entity.PrivateKey.Encrypted {
293+
err := entity.PrivateKey.Decrypt(passphrase)
294+
if err != nil {
295+
t.Errorf("#%d: failed to decrypt key", i)
296+
}
297+
}
298+
for _, subkey := range entity.Subkeys {
299+
if subkey.PrivateKey != nil && subkey.PrivateKey.Encrypted {
300+
err := subkey.PrivateKey.Decrypt(passphrase)
301+
if err != nil {
302+
t.Errorf("#%d: failed to decrypt subkey", i)
303+
}
304+
}
305+
}
306+
}
307+
308+
signed := kring[0]
309+
310+
buf := new(bytes.Buffer)
311+
w, err := Sign(buf, signed, nil /* no hints */, nil)
312+
if err != nil {
313+
t.Errorf("#%d: error in Sign: %s", i, err)
314+
continue
315+
}
316+
317+
const message = "testing"
318+
_, err = w.Write([]byte(message))
319+
if err != nil {
320+
t.Errorf("#%d: error writing plaintext: %s", i, err)
321+
continue
322+
}
323+
err = w.Close()
324+
if err != nil {
325+
t.Errorf("#%d: error closing WriteCloser: %s", i, err)
326+
continue
327+
}
328+
329+
md, err := ReadMessage(buf, kring, nil /* no prompt */, nil)
330+
if err != nil {
331+
t.Errorf("#%d: error reading message: %s", i, err)
332+
continue
333+
}
334+
335+
testTime, _ := time.Parse("2006-01-02", "2013-07-01")
336+
signKey, _ := kring[0].signingKey(testTime)
337+
expectedKeyId := signKey.PublicKey.KeyId
338+
if md.SignedByKeyId != expectedKeyId {
339+
t.Errorf("#%d: message signed by wrong key id, got: %v, want: %v", i, *md.SignedBy, expectedKeyId)
340+
}
341+
if md.SignedBy == nil {
342+
t.Errorf("#%d: failed to find the signing Entity", i)
343+
}
344+
345+
plaintext, err := ioutil.ReadAll(md.UnverifiedBody)
346+
if err != nil {
347+
t.Errorf("#%d: error reading contents: %v", i, err)
348+
continue
349+
}
350+
351+
if string(plaintext) != message {
352+
t.Errorf("#%d: got: %q, want: %q", i, plaintext, message)
353+
}
354+
355+
if md.SignatureError != nil {
356+
t.Errorf("#%d: signature error: %q", i, md.SignatureError)
357+
}
358+
if md.Signature == nil {
359+
t.Error("signature missing")
360+
}
361+
}
362+
}

0 commit comments

Comments
 (0)