github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/engine/pgp_warnings_test.go (about)

     1  package engine
     2  
     3  import (
     4  	"crypto"
     5  	"io"
     6  	"strings"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/stretchr/testify/require"
    11  
    12  	"github.com/keybase/client/go/libkb"
    13  	"github.com/keybase/go-crypto/openpgp"
    14  	"github.com/keybase/go-crypto/openpgp/clearsign"
    15  	"github.com/keybase/go-crypto/openpgp/packet"
    16  	"github.com/keybase/go-crypto/openpgp/s2k"
    17  )
    18  
    19  const pgpWarningsMsg = `Consonantia, there live the blind texts. Separated they live in Bookmarksgrove
    20  right at the coast of the Semantics, a large language ocean. A small river named
    21  Duden flows by their place and supplies it with the necessary regelialia. It is
    22  a paradisematic country, in which roasted parts of sentences fly into your
    23  mouth. Even the all-powerful Pointing has no control about the blind texts it is
    24  an almost unorthographic life One day however a small line of blind text by the
    25  name of Lorem Ipsum decided to leave for the far World of Grammar. The Big Oxmox
    26  advised her not to do so, because there were thousands of bad Commas, wild
    27  Question Marks and devious Semikoli, but the Little Blind Text didn’t listen.
    28  She packed her seven versalia, put her initial into the belt and made herself on
    29  the way. When she reached the first hills of the Italic Mountains, she had a
    30  last view back on the skyline of her hometown Bookmarksgrove, the headline of
    31  Alphabet Village and the subline of her own road, the Line Lane. Pityful a
    32  rethoric question ran over her cheek, then she continued her way. On her way she
    33  met a copy. The copy warned the Little Blind Text, that where it came from it
    34  would have been rewritten a thousand times and everything that was left from its
    35  origin would be the word "and" and the Little Blind Text should turn around and
    36  return to its own, safe country. But nothing the copy said could convince her
    37  and so it didn’t take long until a few insidious Copy Writers ambushed her, made
    38  her drunk with Longe and Parole and dragged her into their agency, where they
    39  abused her for their projects again and again. And if she hasn’t been rewritten,
    40  then they are still using her.Far far away, behind the word mountains, far from
    41  the countries Vokalia and Consonantia, there live the blind texts. Separated
    42  they live in Bookmarksgrove right at the coast of the Semantics, a large
    43  language ocean. A small river named Duden flows by their place and supplies it
    44  with the necessary regelialia. It is a paradisematic country, in which roasted
    45  parts of sentences fly into your mouth. Even the all-powerful Pointing has no
    46  control about the blind texts it is an almost unorthographic life One`
    47  
    48  func generateOpenPGPEntity(ts time.Time, hash crypto.Hash, accepts []crypto.Hash) (*openpgp.Entity, error) {
    49  	// All test keys are RSA-768 because of the ease of generation
    50  	cfg := &packet.Config{
    51  		DefaultHash: hash,
    52  		Time:        func() time.Time { return ts },
    53  		RSABits:     768,
    54  	}
    55  	hashName := libkb.HashToName[hash]
    56  	hashLower := strings.ToLower(hashName)
    57  	entity, err := openpgp.NewEntity("Test "+hashName, "", hashLower+"@example.com", cfg)
    58  	if err != nil {
    59  		return nil, err
    60  	}
    61  
    62  	acceptsConverted := []uint8{}
    63  	for _, h := range accepts {
    64  		if v, ok := s2k.HashToHashId(h); ok {
    65  			acceptsConverted = append(acceptsConverted, v)
    66  		}
    67  	}
    68  
    69  	// Sign all the identities...
    70  	for _, identity := range entity.Identities {
    71  		identity.SelfSignature.PreferredHash = acceptsConverted
    72  		if err := identity.SelfSignature.SignUserId(identity.UserId.Id, entity.PrimaryKey, entity.PrivateKey, cfg); err != nil {
    73  			panic(err)
    74  		}
    75  	}
    76  	// and the subkeys...
    77  	for _, subkey := range entity.Subkeys {
    78  		if err := subkey.Sig.SignKey(subkey.PublicKey, entity.PrivateKey, cfg); err != nil {
    79  			panic(err)
    80  		}
    81  	}
    82  
    83  	return entity, nil
    84  }
    85  
    86  type encryptTest struct {
    87  	Name string // Name of the test
    88  
    89  	DigestHash crypto.Hash // Hash used for msg digests
    90  	AlicesHash crypto.Hash
    91  	BobsHash   crypto.Hash
    92  	Count      int // How many warnings are expected
    93  
    94  	Mode  string // either encrypt, encrypt-and-sign
    95  	Known bool   // whether to check for the key on keybase
    96  }
    97  
    98  type pgpWarningsUserBundle struct {
    99  	tc   libkb.TestContext
   100  	key  *libkb.PGPKeyBundle
   101  	user *FakeUser
   102  }
   103  
   104  func (e encryptTest) test(t *testing.T, users map[string]map[crypto.Hash]*pgpWarningsUserBundle) {
   105  	t.Logf("Processing PGP warnings test - %s", e.Name)
   106  
   107  	now := time.Now()
   108  	supportedHashes := []crypto.Hash{crypto.SHA1, crypto.SHA256}
   109  
   110  	// Alice is the:
   111  	// 1) Party encrypting (and optionally signing) to Bob in "encrypt"
   112  	// 2) The verifier / decrypter in all other scenarios
   113  	if _, ok := users["alice"]; !ok {
   114  		users["alice"] = map[crypto.Hash]*pgpWarningsUserBundle{}
   115  	}
   116  	if _, ok := users["alice"][e.AlicesHash]; !ok {
   117  		tc := SetupEngineTest(t, "PGPEncrypt")
   118  		tc.Tp.APIHeaders = map[string]string{"X-Keybase-Sigchain-Compatibility": "1"}
   119  
   120  		// Generate Alice's keypair. It'll always be SHA256 as we don't allow
   121  		// the generation of weaker selfsigs in Keybase itself.
   122  		aliceKeys, err := generateOpenPGPEntity(now, e.AlicesHash, supportedHashes)
   123  		require.NoError(t, err, "alice's keys generation")
   124  		aliceBundle := libkb.NewPGPKeyBundle(aliceKeys)
   125  
   126  		u := createFakeUserWithPGPSibkeyPregen(tc, aliceBundle)
   127  
   128  		users["alice"][e.AlicesHash] = &pgpWarningsUserBundle{
   129  			tc:   tc,
   130  			key:  aliceBundle,
   131  			user: u,
   132  		}
   133  	}
   134  	alice := users["alice"][e.AlicesHash]
   135  
   136  	// We'll only run engines as Alice because Bob ~is a phony who uses SHA1~.
   137  	// 24-01-2019: We're also phonies :(
   138  	m := NewMetaContextForTest(alice.tc).WithUIs(libkb.UIs{
   139  		LogUI: alice.tc.G.UI.GetLogUI(),
   140  		IdentifyUI: &FakeIdentifyUI{
   141  			Proofs: map[string]string{},
   142  		},
   143  		SecretUI: alice.user.NewSecretUI(),
   144  		PgpUI:    &TestPgpUI{},
   145  	})
   146  
   147  	// Bob is the:
   148  	// 1) Recipient in the "encrypt" scenario
   149  	// 2) Sender in all other scenarios
   150  	// Bob's signatures (both identity sigs and msg digests) can be SHA1.
   151  	// Bob has a cousin called Anonybob who refuses to publish his keys to Keybase,
   152  	// so he sends them over email (the "unknown user" scenario).
   153  	bobName := "bob"
   154  	if !e.Known {
   155  		bobName = "anonybob"
   156  	}
   157  	if _, ok := users[bobName]; !ok {
   158  		users[bobName] = map[crypto.Hash]*pgpWarningsUserBundle{}
   159  	}
   160  	if _, ok := users[bobName][e.BobsHash]; !ok {
   161  		tc := SetupEngineTest(t, "PGPEncrypt")
   162  		tc.Tp.APIHeaders = map[string]string{"X-Keybase-Sigchain-Compatibility": "1"}
   163  
   164  		bobKeys, err := generateOpenPGPEntity(now, e.BobsHash, supportedHashes)
   165  		require.NoError(t, err, "bob's keys generation")
   166  		bobBundle := libkb.NewPGPKeyBundle(bobKeys)
   167  
   168  		var u *FakeUser
   169  		if e.Known {
   170  			u = createFakeUserWithPGPSibkeyPregen(tc, bobBundle)
   171  		} else {
   172  			importEng := NewPGPKeyImportEngine(alice.tc.G, PGPKeyImportEngineArg{
   173  				Pregen:   bobBundle,
   174  				OnlySave: true,
   175  			})
   176  			require.NoError(t, RunEngine2(m, importEng), "importing pubkey failed")
   177  		}
   178  
   179  		users[bobName][e.BobsHash] = &pgpWarningsUserBundle{
   180  			tc:   tc,
   181  			key:  bobBundle,
   182  			user: u,
   183  		}
   184  	}
   185  	bob := users[bobName][e.BobsHash]
   186  
   187  	// Both "encrypt" and "encrypt-and-sign" simply run engine.PGPEncrypt
   188  	if e.Mode == "encrypt" || e.Mode == "encrypt-and-sign" {
   189  		sink := libkb.NewBufferCloser()
   190  		arg := &PGPEncryptArg{
   191  			Recips: []string{bob.user.Username},
   192  			Source: strings.NewReader(pgpWarningsMsg),
   193  			Sink:   sink,
   194  			NoSign: e.Mode == "encrypt",
   195  		}
   196  		eng := NewPGPEncrypt(alice.tc.G, arg)
   197  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   198  
   199  		require.Greaterf(t, len(sink.Bytes()), 0, "no output [%s]", e.Name)
   200  		require.Lenf(t, eng.warnings, e.Count, "warnings count [%s]", e.Name)
   201  		return
   202  	}
   203  
   204  	// "Sign" simply runs engine.PGPSign
   205  	if e.Mode == "sign" {
   206  		sink := libkb.NewBufferCloser()
   207  		arg := &PGPSignArg{
   208  			Source: io.NopCloser(strings.NewReader(pgpWarningsMsg)),
   209  			Sink:   sink,
   210  		}
   211  		eng := NewPGPSignEngine(alice.tc.G, arg)
   212  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   213  
   214  		require.Greaterf(t, len(sink.Bytes()), 0, "no output [%s]", e.Name)
   215  		require.Lenf(t, eng.warnings, e.Count, "warnings count [%s]", e.Name)
   216  		return
   217  	}
   218  
   219  	cfg := &packet.Config{
   220  		DefaultHash: e.DigestHash,
   221  		Time:        func() time.Time { return now },
   222  	}
   223  
   224  	if e.Mode == "verify" {
   225  		// Rather than using PGPSign, we use our custom wrapper methods to make
   226  		// sure that we can set the low level variables.
   227  
   228  		// This time there's no recipient, we're generating a message for the
   229  		// sender / signer / verifier using only the recipient's key.
   230  		var (
   231  			clearsignSink = libkb.NewBufferCloser()
   232  			attachedSink  = libkb.NewBufferCloser()
   233  			detachedSink  = libkb.NewBufferCloser()
   234  		)
   235  
   236  		var signedBy string
   237  		if e.Known {
   238  			signedBy = bob.user.Username
   239  		}
   240  
   241  		// Start with the clearsign sig
   242  		clearsignInput, err := clearsign.Encode(
   243  			clearsignSink,
   244  			bob.key.PrivateKey,
   245  			cfg,
   246  		)
   247  		require.NoErrorf(t, err, "clearsign failure [%s]", e.Name)
   248  		_, err = clearsignInput.Write([]byte(pgpWarningsMsg))
   249  		require.NoErrorf(t, err, "writing to clearsign [%s]", e.Name)
   250  		require.NoErrorf(t, clearsignInput.Close(), "finishing clearsign [%s]", e.Name)
   251  		arg := &PGPVerifyArg{
   252  			Source:   clearsignSink,
   253  			SignedBy: signedBy,
   254  		}
   255  		eng := NewPGPVerify(alice.tc.G, arg)
   256  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   257  		require.Lenf(t, eng.SignatureStatus().Warnings, e.Count, "warnings count [%s]", e.Name)
   258  
   259  		// Then process the attached sig
   260  		attachedInput, _, err := libkb.ArmoredAttachedSign(
   261  			attachedSink,
   262  			*bob.key.Entity,
   263  			nil,
   264  			cfg,
   265  		)
   266  		require.NoErrorf(t, err, "attached sign failure [%s]", e.Name)
   267  		_, err = attachedInput.Write([]byte(pgpWarningsMsg))
   268  		require.NoErrorf(t, err, "writing to attached signer [%s]", e.Name)
   269  		require.NoErrorf(t, attachedInput.Close(), "writing to attached sign [%s]", e.Name)
   270  		arg = &PGPVerifyArg{
   271  			Source:   attachedSink,
   272  			SignedBy: signedBy,
   273  		}
   274  		eng = NewPGPVerify(alice.tc.G, arg)
   275  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   276  		require.Lenf(t, eng.SignatureStatus().Warnings, e.Count, "warnings count [%s]", e.Name)
   277  
   278  		// Detached signatures are probably the easiest
   279  		require.NoError(t, openpgp.ArmoredDetachSignText(
   280  			detachedSink,
   281  			bob.key.Entity,
   282  			strings.NewReader(pgpWarningsMsg),
   283  			cfg,
   284  		), "detached sign failure")
   285  		arg = &PGPVerifyArg{
   286  			Source:    strings.NewReader(pgpWarningsMsg),
   287  			Signature: detachedSink.Bytes(),
   288  			SignedBy:  signedBy,
   289  		}
   290  		eng = NewPGPVerify(alice.tc.G, arg)
   291  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   292  		require.Lenf(t, eng.SignatureStatus().Warnings, e.Count, "warnings count [%s]", e.Name)
   293  
   294  		return
   295  	}
   296  
   297  	if e.Mode == "decrypt" {
   298  		// Mostly the same as decrypt, except we're using different code paths
   299  		// to achieve roughly the same effect.
   300  
   301  		var (
   302  			clearsignSink       = libkb.NewBufferCloser()
   303  			clearsignOutputSink = libkb.NewBufferCloser()
   304  			attachedSink        = libkb.NewBufferCloser()
   305  			attachedOutputSink  = libkb.NewBufferCloser()
   306  		)
   307  
   308  		var signedBy string
   309  		if e.Known {
   310  			signedBy = bob.user.Username
   311  		}
   312  
   313  		// Start with the clearsign sig, which technically isn't even something
   314  		// decryptable.
   315  		clearsignInput, err := clearsign.Encode(
   316  			clearsignSink,
   317  			bob.key.PrivateKey,
   318  			cfg,
   319  		)
   320  		require.NoErrorf(t, err, "clearsign failure [%s]", e.Name)
   321  		_, err = clearsignInput.Write([]byte(pgpWarningsMsg))
   322  		require.NoErrorf(t, err, "writing to clearsign [%s]", e.Name)
   323  		require.NoErrorf(t, clearsignInput.Close(), "finishing clearsign [%s]", e.Name)
   324  		arg := &PGPDecryptArg{
   325  			Sink:         clearsignOutputSink,
   326  			Source:       clearsignSink,
   327  			AssertSigned: true,
   328  			SignedBy:     signedBy,
   329  		}
   330  		eng := NewPGPDecrypt(alice.tc.G, arg)
   331  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   332  
   333  		// TODO: Y2K-1334 Fix this test
   334  		require.Lenf(t, eng.SignatureStatus().Warnings, e.Count, "warnings count [%s]", e.Name)
   335  		// require.Equalf(t, []byte(pgpWarningsMsg), clearsignOutputSink.Bytes(), "output should be the same as the input [%s]", e.Name)
   336  
   337  		// Then process the attached sig
   338  		attachedInput, _, err := libkb.ArmoredAttachedSign(
   339  			attachedSink,
   340  			*bob.key.Entity,
   341  			nil,
   342  			cfg,
   343  		)
   344  		require.NoErrorf(t, err, "attached sign failure [%s]", e.Name)
   345  		_, err = attachedInput.Write([]byte(pgpWarningsMsg))
   346  		require.NoErrorf(t, err, "writing to attached signer [%s]", e.Name)
   347  		require.NoErrorf(t, attachedInput.Close(), "closing the attached signer [%s]", e.Name)
   348  		arg = &PGPDecryptArg{
   349  			Sink:         attachedOutputSink,
   350  			Source:       attachedSink,
   351  			AssertSigned: true,
   352  			SignedBy:     signedBy,
   353  		}
   354  		eng = NewPGPDecrypt(alice.tc.G, arg)
   355  		require.NoErrorf(t, RunEngine2(m, eng), "engine failure [%s]", e.Name)
   356  		require.Lenf(t, eng.SignatureStatus().Warnings, e.Count, "warnings count [%s]", e.Name)
   357  		require.Equalf(t, []byte(pgpWarningsMsg), attachedOutputSink.Bytes(), "output should be the same as the input [%s]", e.Name)
   358  
   359  		return
   360  	}
   361  }
   362  
   363  func TestPGPWarnings(t *testing.T) {
   364  	users := map[string]map[crypto.Hash]*pgpWarningsUserBundle{}
   365  
   366  	// g, _ := errgroup.WithContext(context.Background())
   367  	for _, x := range []encryptTest{
   368  		// Encrypt
   369  		{
   370  			Name:       "Encrypt to a SHA1 recipient",
   371  			AlicesHash: crypto.SHA256,
   372  			BobsHash:   crypto.SHA1,
   373  			Count:      1,
   374  			Mode:       "encrypt",
   375  			Known:      true,
   376  		},
   377  
   378  		// Encrypt and sign
   379  		{
   380  			Name:       "Encrypt and sign to a SHA1 recipient",
   381  			AlicesHash: crypto.SHA256,
   382  			BobsHash:   crypto.SHA1,
   383  			Count:      1,
   384  			Mode:       "encrypt-and-sign",
   385  			Known:      true,
   386  		},
   387  		{
   388  			Name:       "Encrypt and sign to a SHA256 recipient",
   389  			AlicesHash: crypto.SHA256,
   390  			BobsHash:   crypto.SHA256,
   391  			Count:      0,
   392  			Mode:       "encrypt-and-sign",
   393  			Known:      true,
   394  		},
   395  		{
   396  			Name:       "Encrypt and sign from SHA1 to a SHA1 recipient",
   397  			AlicesHash: crypto.SHA1,
   398  			BobsHash:   crypto.SHA1,
   399  			Count:      2,
   400  			Mode:       "encrypt-and-sign",
   401  			Known:      true,
   402  		},
   403  		{
   404  			Name:       "Encrypt and sign from SHA1 to a SHA256 recipient",
   405  			AlicesHash: crypto.SHA1,
   406  			BobsHash:   crypto.SHA256,
   407  			Count:      1,
   408  			Mode:       "encrypt-and-sign",
   409  			Known:      true,
   410  		},
   411  
   412  		// Sign
   413  		{
   414  			Name:       "Sign using SHA1",
   415  			AlicesHash: crypto.SHA1,
   416  			BobsHash:   crypto.SHA256, // unused
   417  			Count:      1,
   418  			Mode:       "sign",
   419  			Known:      true,
   420  		},
   421  		{
   422  			Name:       "Sign using SHA256",
   423  			AlicesHash: crypto.SHA256,
   424  			BobsHash:   crypto.SHA256, // unused
   425  			Count:      0,
   426  			Mode:       "sign",
   427  			Known:      true,
   428  		},
   429  
   430  		// Verify - will run all 3 variants (clearsign / attached / detached)
   431  		{
   432  			Name:       "Verification of a SHA1 sig with a SHA1 self-sig",
   433  			DigestHash: crypto.SHA1,
   434  			AlicesHash: crypto.SHA256,
   435  			BobsHash:   crypto.SHA1,
   436  			Count:      2,
   437  			Mode:       "verify",
   438  			Known:      true,
   439  		},
   440  		{
   441  			Name:       "Verification of a SHA256 sig with a SHA1 self-sig",
   442  			DigestHash: crypto.SHA256,
   443  			AlicesHash: crypto.SHA256,
   444  			BobsHash:   crypto.SHA1,
   445  			Count:      1,
   446  			Mode:       "verify",
   447  		},
   448  		{
   449  			Name:       "Verification of a SHA1 sig with a SHA256 self-sig",
   450  			DigestHash: crypto.SHA1,
   451  			AlicesHash: crypto.SHA256,
   452  			BobsHash:   crypto.SHA256,
   453  			Count:      1,
   454  			Mode:       "verify",
   455  			Known:      true,
   456  		},
   457  		{
   458  			Name:       "Verification of a SHA1 sig with a SHA256 self-sig (unknown)",
   459  			DigestHash: crypto.SHA1,
   460  			AlicesHash: crypto.SHA256,
   461  			BobsHash:   crypto.SHA256,
   462  			Count:      1,
   463  			Mode:       "verify",
   464  			Known:      false,
   465  		},
   466  		{
   467  			Name:       "Verification of a SHA256 sig with a SHA256 self-sig",
   468  			DigestHash: crypto.SHA256,
   469  			AlicesHash: crypto.SHA256,
   470  			BobsHash:   crypto.SHA256,
   471  			Count:      0,
   472  			Mode:       "verify",
   473  		},
   474  
   475  		// Decrypt - will run all 2 variants (clearsign / attached)
   476  		{
   477  			Name:       "Decryption of a SHA1 sig with a SHA1 self-sig",
   478  			DigestHash: crypto.SHA1,
   479  			AlicesHash: crypto.SHA256,
   480  			BobsHash:   crypto.SHA1,
   481  			Count:      2,
   482  			Mode:       "decrypt",
   483  			Known:      true,
   484  		},
   485  		{
   486  			Name:       "Decryption of a SHA256 sig with a SHA1 self-sig",
   487  			DigestHash: crypto.SHA256,
   488  			AlicesHash: crypto.SHA256,
   489  			BobsHash:   crypto.SHA1,
   490  			Count:      1,
   491  			Mode:       "decrypt",
   492  		},
   493  		{
   494  			Name:       "Decryption of a SHA1 sig with a SHA256 self-sig",
   495  			DigestHash: crypto.SHA1,
   496  			AlicesHash: crypto.SHA256,
   497  			BobsHash:   crypto.SHA256,
   498  			Count:      1,
   499  			Mode:       "decrypt",
   500  			Known:      true,
   501  		},
   502  		{
   503  			Name:       "Decryption of a SHA256 sig with a SHA256 self-sig",
   504  			DigestHash: crypto.SHA256,
   505  			AlicesHash: crypto.SHA256,
   506  			BobsHash:   crypto.SHA256,
   507  			Count:      0,
   508  			Mode:       "decrypt",
   509  		},
   510  	} {
   511  		x.test(t, users)
   512  	}
   513  
   514  	for _, hashesToBundles := range users {
   515  		for _, textBundle := range hashesToBundles {
   516  			textBundle.tc.Cleanup()
   517  		}
   518  	}
   519  }