From 371d429001b2ef2e7f06260975c93706dd600b4d Mon Sep 17 00:00:00 2001 From: wussler Date: Wed, 4 Nov 2020 17:40:45 +0100 Subject: [PATCH] WIP: Add compression to API (#91) * Add compression to API * Add docs * Use defaults for a simpler interface * Update x/crypto * Fix ecdsa key types for lib update --- CHANGELOG.md | 11 +++++++++ constants/cipher.go | 3 +++ crypto/base_test.go | 4 ++-- crypto/key.go | 6 ++--- crypto/key_clear.go | 6 ++--- crypto/key_test.go | 18 +++++++++------ crypto/keyring_message.go | 32 ++++++++++++++++++++++---- crypto/keyring_test.go | 4 ++-- crypto/message_test.go | 48 +++++++++++++++++++++++++++++++++++++++ crypto/sessionkey.go | 31 +++++++++++++++++++++---- crypto/sessionkey_test.go | 28 ++++++++++++++++++++++- go.mod | 2 +- go.sum | 15 ++++++++---- 13 files changed, 177 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c6b36ff..62c72e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Security +- Updated underlying crypto library + ### Added - Key Armoring with custom headers ```go @@ -138,6 +141,13 @@ NewPlainMessageFromFile(data []byte, filename string, modTime int) *PlainMessage (msg *PlainMessage) GetModTime() uint32 ``` +- `EncryptWithCompression` to encrypt specifying a compression for asymmetric and session keys +```go +(keyRing *KeyRing) EncryptWithCompression(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) + +(sk *SessionKey) EncryptWithCompression(message *PlainMessage) ([]byte, error) +``` + ### Changed - Improved key and message armoring testing - `EncryptSessionKey` now creates encrypted key packets for each valid encryption key in the provided keyring. @@ -148,6 +158,7 @@ NewPlainMessageFromFile(data []byte, filename string, modTime int) *PlainMessage - The `PlainMessage` struct now contains the fields `Filename` (string) and `Time` (uint32) - All the Decrypt* functions return the filename, type, and time specified in the encrypted message - Improved error wrapping and management +- CI has been moved from travis to Actions, with automated artifacts build ### Fixed - Public key armoring headers diff --git a/constants/cipher.go b/constants/cipher.go index a160e2e..970960c 100644 --- a/constants/cipher.go +++ b/constants/cipher.go @@ -16,3 +16,6 @@ const ( SIGNATURE_NO_VERIFIER int = 2 SIGNATURE_FAILED int = 3 ) + +const DefaultCompression = 2 // ZLIB +const DefaultCompressionLevel = 6 // Corresponds to default -1 for ZLIB diff --git a/crypto/base_test.go b/crypto/base_test.go index 4df7581..4549aa1 100644 --- a/crypto/base_test.go +++ b/crypto/base_test.go @@ -63,8 +63,8 @@ func assertRSACleared(t *testing.T, rsaPriv *rsa.PrivateKey) { } } -func assertEdDSACleared(t *testing.T, priv ed25519.PrivateKey) { - assertMemCleared(t, priv) +func assertEdDSACleared(t *testing.T, priv *ed25519.PrivateKey) { + assertMemCleared(t, *priv) } func assertECDHCleared(t *testing.T, priv *ecdh.PrivateKey) { diff --git a/crypto/key.go b/crypto/key.go index 5e2872a..40adbb3 100644 --- a/crypto/key.go +++ b/crypto/key.go @@ -328,10 +328,10 @@ func (key *Key) Check() (bool, error) { func (key *Key) PrintFingerprints() { for _, subKey := range key.entity.Subkeys { if !subKey.Sig.FlagsValid || subKey.Sig.FlagEncryptStorage || subKey.Sig.FlagEncryptCommunications { - fmt.Println("SubKey:" + hex.EncodeToString(subKey.PublicKey.Fingerprint[:])) + fmt.Println("SubKey:" + hex.EncodeToString(subKey.PublicKey.Fingerprint)) } } - fmt.Println("PrimaryKey:" + hex.EncodeToString(key.entity.PrimaryKey.Fingerprint[:])) + fmt.Println("PrimaryKey:" + hex.EncodeToString(key.entity.PrimaryKey.Fingerprint)) } // GetHexKeyID returns the key ID, hex encoded as a string. @@ -346,7 +346,7 @@ func (key *Key) GetKeyID() uint64 { // GetFingerprint gets the fingerprint from the key. func (key *Key) GetFingerprint() string { - return hex.EncodeToString(key.entity.PrimaryKey.Fingerprint[:]) + return hex.EncodeToString(key.entity.PrimaryKey.Fingerprint) } // GetSHA256Fingerprints computes the SHA256 fingerprints of the key and subkeys. diff --git a/crypto/key_clear.go b/crypto/key_clear.go index caf96a8..d9ec5b2 100644 --- a/crypto/key_clear.go +++ b/crypto/key_clear.go @@ -57,7 +57,7 @@ func clearPrivateKey(privateKey interface{}) error { return clearElGamalPrivateKey(priv) case *ecdsa.PrivateKey: return clearECDSAPrivateKey(priv) - case ed25519.PrivateKey: + case *ed25519.PrivateKey: return clearEdDSAPrivateKey(priv) case *ecdh.PrivateKey: return clearECDHPrivateKey(priv) @@ -115,8 +115,8 @@ func clearECDSAPrivateKey(priv *ecdsa.PrivateKey) error { return nil } -func clearEdDSAPrivateKey(priv ed25519.PrivateKey) error { - clearMem(priv) +func clearEdDSAPrivateKey(priv *ed25519.PrivateKey) error { + clearMem(*priv) return nil } diff --git a/crypto/key_test.go b/crypto/key_test.go index 82e0577..11b9381 100644 --- a/crypto/key_test.go +++ b/crypto/key_test.go @@ -257,17 +257,21 @@ func TestFailCheckIntegrity(t *testing.T) { k1.entity.PrivateKey.PrivateKey = k2.entity.PrivateKey.PrivateKey // Swap private keys - k3, err := k1.Copy() + isVerified, err := k1.Check() if err != nil { - t.Fatal("Expected no error while locking keyring kr3, got:", err) - } - - isVerified, err := k3.Check() - if err != nil { - t.Fatal("Expected no error while checking correct passphrase, got:", err) + t.Fatal("Expected no error while checking key, got:", err) } assert.Exactly(t, false, isVerified) + + serialized, err := k1.Serialize() + if err != nil { + t.Fatal("Expected no error while serializing keyring kr3, got:", err) + } + + _, err = NewKey(serialized) + + assert.Error(t, err) } func TestGetPublicKey(t *testing.T) { diff --git a/crypto/keyring_message.go b/crypto/keyring_message.go index 363ea98..e8853e1 100644 --- a/crypto/keyring_message.go +++ b/crypto/keyring_message.go @@ -6,6 +6,7 @@ import ( "io" "io/ioutil" + "github.com/ProtonMail/gopenpgp/v2/constants" "github.com/pkg/errors" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/packet" @@ -16,7 +17,28 @@ import ( // * message : The plaintext input as a PlainMessage. // * privateKey : (optional) an unlocked private keyring to include signature in the message. func (keyRing *KeyRing) Encrypt(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) { - encrypted, err := asymmetricEncrypt(message, keyRing, privateKey) + config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} + encrypted, err := asymmetricEncrypt(message, keyRing, privateKey, config) + if err != nil { + return nil, err + } + + return NewPGPMessage(encrypted), nil +} + +// EncryptWithCompression encrypts with compression support a PlainMessage to PGPMessage using public/private keys. +// * message : The plain data as a PlainMessage. +// * privateKey : (optional) an unlocked private keyring to include signature in the message. +// * output : The encrypted data as PGPMessage. +func (keyRing *KeyRing) EncryptWithCompression(message *PlainMessage, privateKey *KeyRing) (*PGPMessage, error) { + config := &packet.Config{ + DefaultCipher: packet.CipherAES256, + Time: getTimeGenerator(), + DefaultCompressionAlgo: constants.DefaultCompression, + CompressionConfig: &packet.CompressionConfig{Level: constants.DefaultCompressionLevel}, + } + + encrypted, err := asymmetricEncrypt(message, keyRing, privateKey, config) if err != nil { return nil, err } @@ -68,7 +90,11 @@ func (keyRing *KeyRing) VerifyDetached(message *PlainMessage, signature *PGPSign // ------ INTERNAL FUNCTIONS ------- // Core for encryption+signature functions. -func asymmetricEncrypt(plainMessage *PlainMessage, publicKey, privateKey *KeyRing) ([]byte, error) { +func asymmetricEncrypt( + plainMessage *PlainMessage, + publicKey, privateKey *KeyRing, + config *packet.Config, +) ([]byte, error) { var outBuf bytes.Buffer var encryptWriter io.WriteCloser var signEntity *openpgp.Entity @@ -82,8 +108,6 @@ func asymmetricEncrypt(plainMessage *PlainMessage, publicKey, privateKey *KeyRin } } - config := &packet.Config{DefaultCipher: packet.CipherAES256, Time: getTimeGenerator()} - hints := &openpgp.FileHints{ IsBinary: plainMessage.IsBinary(), FileName: plainMessage.Filename, diff --git a/crypto/keyring_test.go b/crypto/keyring_test.go index b4d1636..1c9b028 100644 --- a/crypto/keyring_test.go +++ b/crypto/keyring_test.go @@ -157,7 +157,7 @@ func TestClearPrivateKey(t *testing.T) { keys := keyRingCopy.GetKeys() assertRSACleared(t, keys[0].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) - assertEdDSACleared(t, keys[1].entity.PrivateKey.PrivateKey.(ed25519.PrivateKey)) + assertEdDSACleared(t, keys[1].entity.PrivateKey.PrivateKey.(*ed25519.PrivateKey)) assertRSACleared(t, keys[2].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) } @@ -175,7 +175,7 @@ func TestClearPrivateWithSubkeys(t *testing.T) { assertRSACleared(t, keys[0].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) assertRSACleared(t, keys[0].entity.Subkeys[0].PrivateKey.PrivateKey.(*rsa.PrivateKey)) - assertEdDSACleared(t, keys[1].entity.PrivateKey.PrivateKey.(ed25519.PrivateKey)) + assertEdDSACleared(t, keys[1].entity.PrivateKey.PrivateKey.(*ed25519.PrivateKey)) assertECDHCleared(t, keys[1].entity.Subkeys[0].PrivateKey.PrivateKey.(*ecdh.PrivateKey)) assertRSACleared(t, keys[2].entity.PrivateKey.PrivateKey.(*rsa.PrivateKey)) diff --git a/crypto/message_test.go b/crypto/message_test.go index 997c35a..31da5ae 100644 --- a/crypto/message_test.go +++ b/crypto/message_test.go @@ -87,6 +87,54 @@ func TestTextMixedMessageDecryptionWithPassword(t *testing.T) { } func TestTextMessageEncryption(t *testing.T) { + var message = NewPlainMessageFromString( + "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", + ) + + ciphertext, err := keyRingTestPublic.Encrypt(message, nil) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + split, err := ciphertext.SeparateKeyAndData(1024, 0) + if err != nil { + t.Fatal("Expected no error when splitting, got:", err) + } + + assert.Len(t, split.GetBinaryDataPacket(), 133) // Assert uncompressed encrypted body length + + decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, nil, 0) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) +} + +func TestTextMessageEncryptionWithCompression(t *testing.T) { + var message = NewPlainMessageFromString( + "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", + ) + + ciphertext, err := keyRingTestPublic.EncryptWithCompression(message, nil) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + split, err := ciphertext.SeparateKeyAndData(1024, 0) + if err != nil { + t.Fatal("Expected no error when splitting, got:", err) + } + + assert.Len(t, split.GetBinaryDataPacket(), 117) // Assert uncompressed encrypted body length + + decrypted, err := keyRingTestPrivate.Decrypt(ciphertext, nil, 0) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) +} + +func TestTextMessageEncryptionWithSignature(t *testing.T) { var message = NewPlainMessageFromString("plain text") ciphertext, err := keyRingTestPublic.Encrypt(message, keyRingTestPrivate) diff --git a/crypto/sessionkey.go b/crypto/sessionkey.go index b63b116..a259d8d 100644 --- a/crypto/sessionkey.go +++ b/crypto/sessionkey.go @@ -114,9 +114,6 @@ func newSessionKeyFromEncrypted(ek *packet.EncryptedKey) (*SessionKey, error) { // * message : The plain data as a PlainMessage. // * output : The encrypted data as PGPMessage. func (sk *SessionKey) Encrypt(message *PlainMessage) ([]byte, error) { - var encBuf bytes.Buffer - var encryptWriter io.WriteCloser - dc, err := sk.GetCipherFunc() if err != nil { return nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") @@ -127,7 +124,33 @@ func (sk *SessionKey) Encrypt(message *PlainMessage) ([]byte, error) { DefaultCipher: dc, } - encryptWriter, err = packet.SerializeSymmetricallyEncrypted(&encBuf, config.Cipher(), sk.Key, config) + return encryptWithSessionKey(message, sk, config) +} + +// EncryptWithCompression encrypts with compression support a PlainMessage to PGPMessage with a SessionKey. +// * message : The plain data as a PlainMessage. +// * output : The encrypted data as PGPMessage. +func (sk *SessionKey) EncryptWithCompression(message *PlainMessage) ([]byte, error) { + dc, err := sk.GetCipherFunc() + if err != nil { + return nil, errors.Wrap(err, "gopenpgp: unable to encrypt with session key") + } + + config := &packet.Config{ + Time: getTimeGenerator(), + DefaultCipher: dc, + DefaultCompressionAlgo: constants.DefaultCompression, + CompressionConfig: &packet.CompressionConfig{Level: constants.DefaultCompressionLevel}, + } + + return encryptWithSessionKey(message, sk, config) +} + +func encryptWithSessionKey(message *PlainMessage, sk *SessionKey, config *packet.Config) ([]byte, error) { + var encBuf bytes.Buffer + var encryptWriter io.WriteCloser + + encryptWriter, err := packet.SerializeSymmetricallyEncrypted(&encBuf, config.Cipher(), sk.Key, config) if err != nil { return nil, errors.Wrap(err, "gopenpgp: unable to encrypt") } diff --git a/crypto/sessionkey_test.go b/crypto/sessionkey_test.go index 974f694..2c4aaa8 100644 --- a/crypto/sessionkey_test.go +++ b/crypto/sessionkey_test.go @@ -101,13 +101,18 @@ func TestSymmetricKeyPacketWrongSize(t *testing.T) { } func TestDataPacketEncryption(t *testing.T) { - var message = NewPlainMessageFromString("The secret code is... 1, 2, 3, 4, 5") + var message = NewPlainMessageFromString( + "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", + ) // Encrypt data with session key dataPacket, err := testSessionKey.Encrypt(message) if err != nil { t.Fatal("Expected no error when encrypting, got:", err) } + + assert.Len(t, dataPacket, 133) // Assert uncompressed encrypted body length + // Decrypt data with wrong session key wrongKey := SessionKey{ Key: []byte("wrong pass"), @@ -184,3 +189,24 @@ func TestSessionKeyClear(t *testing.T) { testSessionKey.Clear() assertMemCleared(t, testSessionKey.Key) } + +func TestDataPacketEncryptionWithCompression(t *testing.T) { + var message = NewPlainMessageFromString( + "The secret code is... 1, 2, 3, 4, 5. I repeat: the secret code is... 1, 2, 3, 4, 5", + ) + + // Encrypt data with session key + dataPacket, err := testSessionKey.EncryptWithCompression(message) + if err != nil { + t.Fatal("Expected no error when encrypting, got:", err) + } + + assert.Len(t, dataPacket, 117) // Assert compressed encrypted body length + + // Decrypt data with the good session key + decrypted, err := testSessionKey.Decrypt(dataPacket) + if err != nil { + t.Fatal("Expected no error when decrypting, got:", err) + } + assert.Exactly(t, message.GetString(), decrypted.GetString()) +} diff --git a/go.mod b/go.mod index 8fb6265..9ed7888 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,6 @@ require ( golang.org/x/mobile v0.0.0-20200801112145-973feb4309de ) -replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c +replace golang.org/x/crypto => github.com/ProtonMail/crypto v0.0.0-20201104134830-fcb5d97d611a replace golang.org/x/mobile => github.com/zhj4478/mobile v0.0.0-20201014085805-7a2d68bf792f diff --git a/go.sum b/go.sum index 8588bd9..63f6b3a 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,14 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c h1:DAvlgde2Stu18slmjwikiMPs/CKPV35wSvmJS34z0FU= -github.com/ProtonMail/crypto v0.0.0-20200416114516-1fa7f403fb9c/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= +github.com/ProtonMail/crypto v0.0.0-20201016191319-576ad9c42ffa h1:RnTRazSOTTBw0S2D0dK6pbC0uEYtHFdO3YQo8lI6coc= +github.com/ProtonMail/crypto v0.0.0-20201016191319-576ad9c42ffa/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= +github.com/ProtonMail/crypto v0.0.0-20201019194525-d010555f6cb5 h1:VgYclRLmViKcELmBf27PajBM/XFSlRLf+fUacMC5ahM= +github.com/ProtonMail/crypto v0.0.0-20201019194525-d010555f6cb5/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= +github.com/ProtonMail/crypto v0.0.0-20201020131021-8e70b29752f8 h1:L3xFiZBIG8YCbF+DaQyNJ6QwmVhLsd4XQtH20AtZGIE= +github.com/ProtonMail/crypto v0.0.0-20201020131021-8e70b29752f8/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= +github.com/ProtonMail/crypto v0.0.0-20201022141144-3fe6b6992c0f h1:CrqdTsoF7teMqQok+iHUx3yjYJfkpDuU7y/nIxRJ2rY= +github.com/ProtonMail/crypto v0.0.0-20201022141144-3fe6b6992c0f/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= +github.com/ProtonMail/crypto v0.0.0-20201104134830-fcb5d97d611a h1:1zaMXAQgYCo4ca10i6CKcKbd+M3phLp5VFP+f+VrbSI= +github.com/ProtonMail/crypto v0.0.0-20201104134830-fcb5d97d611a/go.mod h1:Pxr7w4gA2ikI4sWyYwEffm+oew1WAJHzG1SiDpQMkrI= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a h1:W6RrgN/sTxg1msqzFFb+G80MFmpjMw61IU+slm+wln4= github.com/ProtonMail/go-mime v0.0.0-20190923161245-9b5a4261663a/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -19,7 +27,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/zhj4478/mobile v0.0.0-20201014085805-7a2d68bf792f h1:3NX1KS08WQ2sF4EYpqlpWlBDpxpcaIkhywFAKQM1iYQ= github.com/zhj4478/mobile v0.0.0-20201014085805-7a2d68bf792f/go.mod h1:skQtrUTUwhdJvXM/2KKJzY8pDgNr9I/FOMqDVRPBUS4= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -41,10 +48,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69 h1:yBHHx+XZqXJBm6Exke3N7V9gnlsyXxoCPEb1yVenjfk= golang.org/x/tools v0.0.0-20200117012304-6edc0a871e69/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=