//nolint:testpackage // white-box tests exercise unexported internals package mfer import ( "bytes" "context" "crypto" "crypto/dsa" //nolint:staticcheck // SA1019: tests need a DSA key to refuse "io" "math/big" "os" "path/filepath" "slices" "strconv" "strings" "testing" "time" "github.com/ProtonMail/go-crypto/openpgp" "github.com/ProtonMail/go-crypto/openpgp/armor" "github.com/ProtonMail/go-crypto/openpgp/elgamal" pgperrors "github.com/ProtonMail/go-crypto/openpgp/errors" "github.com/ProtonMail/go-crypto/openpgp/packet" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" ) // newTestKey returns a new Ed25519 key, which is quick to make, for // "MFER Test Key ". config may set when it is made and how // long it lasts. func newTestKey(t *testing.T, config *packet.Config) *openpgp.Entity { t.Helper() if config == nil { config = &packet.Config{} } config.Algorithm = packet.PubKeyAlgoEdDSA key, err := openpgp.NewEntity("MFER Test Key", "", "test@mfer.test", config) require.NoError(t, err) return key } // armoredSecretKeys returns keys with their secret keys in one armored // block, as gpg --export-secret-keys --armor writes them. func armoredSecretKeys(t *testing.T, keys ...*openpgp.Entity) []byte { t.Helper() var buf bytes.Buffer w, err := armor.Encode(&buf, openpgp.PrivateKeyType, nil) require.NoError(t, err) for _, key := range keys { require.NoError(t, key.SerializePrivateWithoutSigning(w, nil)) } require.NoError(t, w.Close()) return buf.Bytes() } // armoredPublicKeys returns the public parts of keys in one armored block, // as gpg --export --armor writes them. func armoredPublicKeys(t *testing.T, keys ...*openpgp.Entity) []byte { t.Helper() var buf bytes.Buffer w, err := armor.Encode(&buf, openpgp.PublicKeyType, nil) require.NoError(t, err) for _, key := range keys { require.NoError(t, key.Serialize(w)) } require.NoError(t, w.Close()) return buf.Bytes() } // testSigningOptions returns signing options for a new key with no // passphrase. func testSigningOptions(t *testing.T) *SigningOptions { t.Helper() return &SigningOptions{SecretKey: armoredSecretKeys(t, newTestKey(t, nil))} } // joinArmored returns the armored block first followed by the armored // block second on the next line. armor.Encode ends a block without a // newline, unlike gpg, and a block only starts at the start of a line. func joinArmored(first, second []byte) []byte { return slices.Concat(first, []byte("\n"), second) } // signedTestManifest returns a manifest of one file signed with opts. func signedTestManifest(t *testing.T, opts *SigningOptions) []byte { t.Helper() b := NewBuilder() b.SetSigningOptions(opts) content := []byte("signed file content") _, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, bytes.NewReader(content), nil) require.NoError(t, err) var buf bytes.Buffer require.NoError(t, b.Build(context.Background(), &buf)) return buf.Bytes() } // rewriteOuter returns manifest with its outer message changed by edit. // A signature stays good as long as edit leaves the UUID and hash alone. func rewriteOuter(t *testing.T, manifest []byte, edit func(*MFFileOuter)) []byte { t.Helper() outer := new(MFFileOuter) require.NoError(t, proto.Unmarshal(manifest[len(MAGIC):], outer)) edit(outer) data, err := proto.Marshal(outer) require.NoError(t, err) return append([]byte(MAGIC), data...) } func TestBuilderWithSigning(t *testing.T) { t.Parallel() key := newTestKey(t, nil) // Create a builder with signing options b := NewBuilder() b.SetSigningOptions(&SigningOptions{SecretKey: armoredSecretKeys(t, key)}) // Add a test file content := []byte("test file content") reader := bytes.NewReader(content) _, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil) require.NoError(t, err) // Build the manifest var buf bytes.Buffer err = b.Build(context.Background(), &buf) require.NoError(t, err) // Parse the manifest and verify signature fields are populated manifest, err := NewManifestFromReader(&buf) require.NoError(t, err) require.NotNil(t, manifest.pbOuter) assert.NotEmpty(t, manifest.pbOuter.GetSignature(), "signature should be populated") assert.NotEmpty(t, manifest.pbOuter.GetSigningPubKey(), "signing public key should be populated") // The signer is the key's fingerprint in 40 upper-case hex characters. assert.Equal(t, fingerprint(key), string(manifest.pbOuter.GetSigner())) assert.Regexp(t, "^[0-9A-F]{40}$", string(manifest.pbOuter.GetSigner())) // Verify signature is a valid PGP signature assert.Contains(t, string(manifest.pbOuter.GetSignature()), "-----BEGIN PGP SIGNATURE-----") // Verify public key is a valid PGP public key block assert.Contains(t, string(manifest.pbOuter.GetSigningPubKey()), "-----BEGIN PGP PUBLIC KEY BLOCK-----") } // TestManifestSigner checks that Signer gives the signing key's // fingerprint for a signed manifest, and nil once the signature is taken // out, although the signer field is still there. func TestManifestSigner(t *testing.T) { t.Parallel() key := newTestKey(t, nil) data := signedTestManifest(t, &SigningOptions{SecretKey: armoredSecretKeys(t, key)}) signed, err := NewManifestFromReader(bytes.NewReader(data)) require.NoError(t, err) assert.Equal(t, fingerprint(key), string(signed.Signer())) unsigned, err := NewManifestFromReader(bytes.NewReader( rewriteOuter(t, data, func(outer *MFFileOuter) { outer.Signature = nil }))) require.NoError(t, err) assert.Nil(t, unsigned.Signer()) } func TestScannerWithSigning(t *testing.T) { t.Parallel() // Create in-memory filesystem with test files fs := afero.NewMemMapFs() require.NoError(t, fs.MkdirAll("/testdir", 0o755)) require.NoError(t, afero.WriteFile(fs, "/testdir/file1.txt", []byte("content1"), 0o644)) require.NoError(t, afero.WriteFile(fs, "/testdir/file2.txt", []byte("content2"), 0o644)) // Create scanner with signing options opts := &ScannerOptions{ Fs: fs, SigningOptions: testSigningOptions(t), } s := NewScannerWithOptions(opts) // Enumerate files require.NoError(t, s.EnumeratePath("/testdir", nil)) assert.Equal(t, FileCount(2), s.FileCount()) // Generate signed manifest var buf bytes.Buffer require.NoError(t, s.ToManifest(context.Background(), &buf, nil)) // Parse and verify manifest, err := NewManifestFromReader(&buf) require.NoError(t, err) assert.NotEmpty(t, manifest.pbOuter.GetSignature()) assert.NotEmpty(t, manifest.pbOuter.GetSigner()) assert.NotEmpty(t, manifest.pbOuter.GetSigningPubKey()) } // TestSigningWithBinarySecretKey signs with a secret key that is not // armored, as gpg --export-secret-keys writes it without --armor. func TestSigningWithBinarySecretKey(t *testing.T) { t.Parallel() var secretKey bytes.Buffer require.NoError(t, newTestKey(t, nil).SerializePrivateWithoutSigning(&secretKey, nil)) _, err := NewManifestFromReader(bytes.NewReader( signedTestManifest(t, &SigningOptions{SecretKey: secretKey.Bytes()}))) require.NoError(t, err) } // TestSigningWithProtectedKey signs with a key protected by a passphrase: // with the passphrase, without one, and with a wrong one. func TestSigningWithProtectedKey(t *testing.T) { t.Parallel() key := newTestKey(t, nil) require.NoError(t, key.EncryptPrivateKeys([]byte("right"), nil)) secretKey := armoredSecretKeys(t, key) protected, err := SecretKeyIsProtected(secretKey) require.NoError(t, err) assert.True(t, protected) _, err = NewManifestFromReader(bytes.NewReader(signedTestManifest(t, &SigningOptions{SecretKey: secretKey, Passphrase: []byte("right")}))) require.NoError(t, err) b := NewBuilder() b.SetSigningOptions(&SigningOptions{SecretKey: secretKey}) require.ErrorIs(t, b.Build(context.Background(), io.Discard), errNoPassphrase) b.SetSigningOptions(&SigningOptions{ SecretKey: secretKey, Passphrase: []byte("wrong"), }) assert.ErrorContains(t, b.Build(context.Background(), io.Discard), "unlock signing key") } func TestSecretKeyIsProtectedWithoutPassphrase(t *testing.T) { t.Parallel() protected, err := SecretKeyIsProtected(testSigningOptions(t).SecretKey) require.NoError(t, err) assert.False(t, protected) } // TestSecretKeyFingerprint checks that SecretKeyFingerprint gives the // signer that a manifest signed with the key records. func TestSecretKeyFingerprint(t *testing.T) { t.Parallel() opts := testSigningOptions(t) got, err := SecretKeyFingerprint(opts.SecretKey) require.NoError(t, err) m, err := NewManifestFromReader(bytes.NewReader(signedTestManifest(t, opts))) require.NoError(t, err) assert.Equal(t, string(m.Signer()), got) } // TestSigningKeyFileWithTwoKeys signs with a key file that holds two keys, // as gpg writes it for a user ID that two keys have. It names no one key // to sign with, so signing must fail. func TestSigningKeyFileWithTwoKeys(t *testing.T) { t.Parallel() b := NewBuilder() b.SetSigningOptions(&SigningOptions{SecretKey: armoredSecretKeys(t, newTestKey(t, nil), newTestKey(t, nil))}) err := b.Build(context.Background(), io.Discard) require.ErrorIs(t, err, errKeyCount) assert.EqualError(t, err, "signing key file must hold exactly one key, found 2") } // TestSigningKeyFileWithoutSecretKey signs with a file that holds only a // public key. func TestSigningKeyFileWithoutSecretKey(t *testing.T) { t.Parallel() b := NewBuilder() b.SetSigningOptions(&SigningOptions{ SecretKey: armoredPublicKeys(t, newTestKey(t, nil)), }) require.ErrorIs(t, b.Build(context.Background(), io.Discard), errNoSecretKey) } // TestCheckSigningKeyRefusesKeyThatCannotSign checks a key that can sign, // and keys that read and need no passphrase but cannot sign now: one that // expired in 2020 and one that has been revoked. CheckSigningKey must // refuse each of the two, as signing a manifest with it would fail. func TestCheckSigningKeyRefusesKeyThatCannotSign(t *testing.T) { t.Parallel() require.NoError(t, CheckSigningKey(testSigningOptions(t))) made := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) expired := newTestKey(t, &packet.Config{ Time: func() time.Time { return made }, KeyLifetimeSecs: uint32((24 * time.Hour).Seconds()), }) revoked := newTestKey(t, nil) require.NoError(t, revoked.RevokeKey(packet.KeyRetired, "", nil)) for name, key := range map[string]*openpgp.Entity{ "expired": expired, "revoked": revoked, } { t.Run(name, func(t *testing.T) { t.Parallel() assert.ErrorContains(t, CheckSigningKey( &SigningOptions{SecretKey: armoredSecretKeys(t, key)}), "signing key cannot sign") }) } } // TestSigningRefusesVersion6Key signs with an OpenPGP version 6 key, whose // fingerprint is 64 hex characters. mfer signs only with version 4 keys. func TestSigningRefusesVersion6Key(t *testing.T) { t.Parallel() key, err := openpgp.NewEntity("MFER Test Key", "", "test@mfer.test", &packet.Config{V6Keys: true, Algorithm: packet.PubKeyAlgoEd25519}) require.NoError(t, err) secretKey := armoredSecretKeys(t, key) _, err = SecretKeyIsProtected(secretKey) require.ErrorIs(t, err, errNotV4Key) b := NewBuilder() b.SetSigningOptions(&SigningOptions{SecretKey: secretKey}) require.ErrorIs(t, b.Build(context.Background(), io.Discard), errNotV4Key) } // TestMalformedArmorIsNamed reads a signing key file, a signature and an // embedded public key block whose armor is malformed: a header line with // no colon, or no blank line after the BEGIN line. Each must fail saying // the armor is malformed. func TestMalformedArmorIsNamed(t *testing.T) { t.Parallel() opts := testSigningOptions(t) manifest := signedTestManifest(t, opts) for name, change := range map[string]func([]byte) []byte{ "header line with no colon": func(block []byte) []byte { return bytes.Replace(block, []byte("-----\n"), []byte("-----\nno colon\n"), 1) }, "no blank line after the BEGIN line": func(block []byte) []byte { return bytes.Replace(block, []byte("-----\n\n"), []byte("-----\n"), 1) }, } { t.Run(name, func(t *testing.T) { t.Parallel() _, err := SecretKeyIsProtected(change(opts.SecretKey)) require.ErrorIs(t, err, errMalformedArmor) for _, changed := range [][]byte{ rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.Signature = change(outer.GetSignature()) }), rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.SigningPubKey = change(outer.GetSigningPubKey()) }), } { _, err = NewManifestFromReader(bytes.NewReader(changed)) require.ErrorIs(t, err, errMalformedArmor) } }) } } func TestVerifySignature(t *testing.T) { t.Parallel() key := newTestKey(t, nil) data := []byte("test data to sign and verify") var armored, binary bytes.Buffer require.NoError(t, openpgp.ArmoredDetachSign(&armored, key, bytes.NewReader(data), nil)) require.NoError(t, openpgp.DetachSign(&binary, key, bytes.NewReader(data), nil)) pubKey, err := armoredPublicKey(key) require.NoError(t, err) var binaryPubKey bytes.Buffer require.NoError(t, key.Serialize(&binaryPubKey)) // Verifying names the key that made the signature, whether the // signature and key are armored or not. signer, err := verifySignature(data, armored.Bytes(), pubKey) require.NoError(t, err) assert.Equal(t, fingerprint(key), signer) signer, err = verifySignature(data, binary.Bytes(), binaryPubKey.Bytes()) require.NoError(t, err) assert.Equal(t, fingerprint(key), signer) // A signature over other data is bad. _, err = verifySignature([]byte("different data"), armored.Bytes(), pubKey) require.Error(t, err) // A public key that is not one cannot verify anything. _, err = verifySignature(data, armored.Bytes(), []byte("not a public key")) assert.Error(t, err) } // TestVerifySignatureKeyExpiredSince verifies two signatures made in 2020 // by a key that expired a day after it was made. A signature with no // expiry time is still good, as is one made then by a signing subkey that // expired a day after it was made. One that expired an hour after it was // made is refused, although the key has expired too. func TestVerifySignatureKeyExpiredSince(t *testing.T) { t.Parallel() made := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) config := &packet.Config{ Time: func() time.Time { return made }, KeyLifetimeSecs: uint32((24 * time.Hour).Seconds()), } key := newTestKey(t, config) data := []byte("signed in 2020") var sig, expiringSig bytes.Buffer require.NoError(t, openpgp.ArmoredDetachSign(&sig, key, bytes.NewReader(data), config)) require.NoError(t, openpgp.ArmoredDetachSign(&expiringSig, key, bytes.NewReader(data), &packet.Config{ Time: config.Time, SigLifetimeSecs: uint32(time.Hour.Seconds()), })) pubKey, err := armoredPublicKey(key) require.NoError(t, err) signer, err := verifySignature(data, sig.Bytes(), pubKey) require.NoError(t, err) assert.Equal(t, fingerprint(key), signer) withSubkey := newTestKey(t, &packet.Config{Time: config.Time}) require.NoError(t, withSubkey.AddSigningSubkey(&packet.Config{ Algorithm: packet.PubKeyAlgoEdDSA, Time: config.Time, KeyLifetimeSecs: config.KeyLifetimeSecs, })) var subkeySig bytes.Buffer require.NoError(t, openpgp.ArmoredDetachSign(&subkeySig, withSubkey, bytes.NewReader(data), config)) subkeyPubKey, err := armoredPublicKey(withSubkey) require.NoError(t, err) _, err = verifySignature(data, subkeySig.Bytes(), subkeyPubKey) require.NoError(t, err) _, err = verifySignature(data, expiringSig.Bytes(), pubKey) require.ErrorIs(t, err, pgperrors.ErrSignatureExpired) } // signDated returns a detached signature over data by signer, dated when. // openpgp.DetachSign signs only with a key that is valid at the time its // config gives, so this makes the signature packet without that check. func signDated(t *testing.T, signer *packet.PrivateKey, data []byte, when time.Time, ) []byte { t.Helper() sig := &packet.Signature{ SigType: packet.SigTypeBinary, PubKeyAlgo: signer.PubKeyAlgo, Hash: crypto.SHA256, CreationTime: when, IssuerKeyId: &signer.KeyId, } h, err := sig.PrepareSign(nil) require.NoError(t, err) _, err = h.Write(data) require.NoError(t, err) require.NoError(t, sig.Sign(h, signer, nil)) var buf bytes.Buffer require.NoError(t, sig.Serialize(&buf)) return buf.Bytes() } // TestVerifySignatureRefusesKeyNotValidWhenSigned verifies signatures // dated when the key or subkey that made them was not valid: after the key // expired; after the key's self-signature, or the signing subkey's binding // signature, expired, the key or subkey having expired since as well; and // before the key was made, also by a key made in 2100. Each must be // refused. func TestVerifySignatureRefusesKeyNotValidWhenSigned(t *testing.T) { t.Parallel() made := time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) twoDaysLater := made.Add(48 * time.Hour) day := uint32((24 * time.Hour).Seconds()) clock := func(when time.Time) func() time.Time { return func() time.Time { return when } } expiredAfterADay := newTestKey(t, &packet.Config{ Time: clock(made), KeyLifetimeSecs: day, }) selfSignatureExpiredAfterADay := newTestKey(t, &packet.Config{ Time: clock(made), KeyLifetimeSecs: 30 * day, SigLifetimeSecs: day, }) withSubkey := newTestKey(t, &packet.Config{Time: clock(made)}) require.NoError(t, withSubkey.AddSigningSubkey(&packet.Config{ Algorithm: packet.PubKeyAlgoEdDSA, Time: clock(made), KeyLifetimeSecs: 30 * day, SigLifetimeSecs: day, })) subkey := withSubkey.Subkeys[len(withSubkey.Subkeys)-1].PrivateKey madeADayLater := newTestKey(t, &packet.Config{ Time: clock(made.Add(24 * time.Hour)), }) madeIn2100 := newTestKey(t, &packet.Config{ Time: clock(time.Date(2100, 1, 1, 0, 0, 0, 0, time.UTC)), }) for name, test := range map[string]struct { key *openpgp.Entity signer *packet.PrivateKey when time.Time }{ "after the key expired": { expiredAfterADay, expiredAfterADay.PrivateKey, twoDaysLater, }, "after the self-signature expired": { selfSignatureExpiredAfterADay, selfSignatureExpiredAfterADay.PrivateKey, twoDaysLater, }, "after the binding signature expired": { withSubkey, subkey, twoDaysLater, }, "before the key was made": { madeADayLater, madeADayLater.PrivateKey, made, }, "before a key made in 2100": { madeIn2100, madeIn2100.PrivateKey, made, }, } { t.Run(name, func(t *testing.T) { t.Parallel() data := []byte("signed in 2020") pubKey, err := armoredPublicKey(test.key) require.NoError(t, err) _, err = verifySignature(data, signDated(t, test.signer, data, test.when), pubKey) require.ErrorIs(t, err, errKeyNotValidWhenSigned) }) } } func TestManifestSignatureVerification(t *testing.T) { t.Parallel() // Parse the manifest - signature should be verified during load manifest, err := NewManifestFromReader(bytes.NewReader( signedTestManifest(t, testSigningOptions(t)))) require.NoError(t, err) require.NotNil(t, manifest) // Signature should be present and valid assert.NotEmpty(t, manifest.pbOuter.GetSignature()) } func TestManifestTamperedSignatureFails(t *testing.T) { t.Parallel() // Change one character of the signature's base64 body, which starts // after the blank line that ends the armor headers. data := rewriteOuter(t, signedTestManifest(t, testSigningOptions(t)), func(outer *MFFileOuter) { sig := outer.GetSignature() i := bytes.Index(sig, []byte("\n\n")) + len("\n\n") + 20 sig[i]++ }) // Try to load the tampered manifest - should fail _, err := NewManifestFromReader(bytes.NewReader(data)) assert.Error(t, err) } // TestManifestSignedByGPGLoads loads the signed seed of // FuzzNewManifestFromReader, a manifest signed with gpg before mfer signed // and verified manifests itself. func TestManifestSignedByGPGLoads(t *testing.T) { t.Parallel() seed, err := os.ReadFile(filepath.Join( "testdata", "fuzz", "FuzzNewManifestFromReader", "signed")) require.NoError(t, err) // After its header line the seed holds the manifest as []byte("..."). _, quoted, found := strings.Cut(string(seed), "[]byte(") require.True(t, found) manifest, err := strconv.Unquote( strings.TrimSuffix(strings.TrimSpace(quoted), ")")) require.NoError(t, err) m, err := NewManifestFromReader(strings.NewReader(manifest)) require.NoError(t, err) assert.Equal(t, "4F562BFB863FDC6B51B4EE88872A51176CEF23AE", string(m.pbOuter.GetSigner())) } // TestManifestRefusesSecondEmbeddedKey loads manifests whose embedded // public key block holds another key besides the key that signed it: as a // public key, as a secret key with no user ID, which openpgp.ReadKeyRing // skips, and written as a subkey packet at the start of the block, which // openpgp.ReadKeyRing reads as a primary key. Loading must refuse each, // although the signature is good and the signer field names the key that // made it. func TestManifestRefusesSecondEmbeddedKey(t *testing.T) { t.Parallel() other := newTestKey(t, nil) signer := newTestKey(t, nil) manifest := signedTestManifest(t, &SigningOptions{SecretKey: armoredSecretKeys(t, signer)}) otherWithoutUserID := newTestKey(t, nil) otherWithoutUserID.Identities = map[string]*openpgp.Identity{} otherAsSubkey := newTestKey(t, nil) otherAsSubkey.PrimaryKey.IsSubkey = true for name, block := range map[string][]byte{ "public key": armoredPublicKeys(t, other, signer), "secret key without user ID": armoredSecretKeys(t, signer, otherWithoutUserID), "subkey packet first": armoredPublicKeys(t, otherAsSubkey, signer), } { t.Run(name, func(t *testing.T) { t.Parallel() embedded := rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.SigningPubKey = block }) _, err := NewManifestFromReader(bytes.NewReader(embedded)) require.ErrorIs(t, err, errKeyCount) }) } } // TestManifestRefusesSecondEmbeddedKeyWithoutUserID loads a manifest whose // embedded public key block holds, before the key that signed it, another // key with no user ID, which openpgp.ReadKeyRing skips. Loading must // refuse it: the block holds two keys. func TestManifestRefusesSecondEmbeddedKeyWithoutUserID(t *testing.T) { t.Parallel() other := newTestKey(t, nil) other.Identities = map[string]*openpgp.Identity{} signer := newTestKey(t, nil) manifest := rewriteOuter(t, signedTestManifest(t, &SigningOptions{SecretKey: armoredSecretKeys(t, signer)}), func(outer *MFFileOuter) { outer.SigningPubKey = armoredPublicKeys(t, other, signer) }) _, err := NewManifestFromReader(bytes.NewReader(manifest)) require.ErrorIs(t, err, errKeyCount) } // dsaKeyPacket returns a public key packet holding a DSA key, or a public // subkey packet when isSubkey. Its numbers are not a working key: loading // must refuse the packet before it uses them. func dsaKeyPacket(t *testing.T, isSubkey bool) []byte { t.Helper() key := packet.NewDSAPublicKey(time.Now(), &dsa.PublicKey{ P: big.NewInt(23), Q: big.NewInt(11), G: big.NewInt(4), Y: big.NewInt(8), }) key.IsSubkey = isSubkey var buf bytes.Buffer require.NoError(t, key.Serialize(&buf)) return buf.Bytes() } // TestManifestRefusesDSAKey loads manifests whose embedded public key // block holds a DSA key: alone, or as a subkey after the key that signed // the manifest. openpgp.ReadKeyRing checks a key's self-signatures, which // for a DSA key with very large numbers takes minutes each. Loading must // refuse each block before that. func TestManifestRefusesDSAKey(t *testing.T) { t.Parallel() signer := newTestKey(t, nil) manifest := signedTestManifest(t, &SigningOptions{SecretKey: armoredSecretKeys(t, signer)}) var signerKey bytes.Buffer require.NoError(t, signer.Serialize(&signerKey)) for name, block := range map[string][]byte{ "DSA key": dsaKeyPacket(t, false), "DSA subkey": slices.Concat(signerKey.Bytes(), dsaKeyPacket(t, true)), } { t.Run(name, func(t *testing.T) { t.Parallel() embedded := rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.SigningPubKey = block }) _, err := NewManifestFromReader(bytes.NewReader(embedded)) require.ErrorIs(t, err, errDSAKey) }) } } // elGamalSecretSubkeyPacket returns a secret subkey packet holding an // ElGamal key. Its numbers are not a working key: loading must refuse the // packet before it uses them. func elGamalSecretSubkeyPacket(t *testing.T) []byte { t.Helper() key := packet.NewElGamalPrivateKey(time.Now(), &elgamal.PrivateKey{ P: big.NewInt(23), G: big.NewInt(4), Y: big.NewInt(8), X: big.NewInt(3), }) key.IsSubkey = true var buf bytes.Buffer require.NoError(t, key.Serialize(&buf)) return buf.Bytes() } // TestManifestRefusesSecretKey loads manifests whose embedded public key // block holds a secret key: the key that signed the manifest with its // secret key, or its public key followed by an ElGamal secret subkey. // openpgp.ReadKeyRing checks the numbers of every secret key it reads, // which for an ElGamal key with a very large prime takes minutes. Loading // must refuse each block before that. func TestManifestRefusesSecretKey(t *testing.T) { t.Parallel() signer := newTestKey(t, nil) manifest := signedTestManifest(t, &SigningOptions{SecretKey: armoredSecretKeys(t, signer)}) var signerKey bytes.Buffer require.NoError(t, signer.Serialize(&signerKey)) for _, block := range [][]byte{ armoredSecretKeys(t, signer), slices.Concat(signerKey.Bytes(), elGamalSecretSubkeyPacket(t)), } { embedded := rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.SigningPubKey = block }) _, err := NewManifestFromReader(bytes.NewReader(embedded)) require.ErrorIs(t, err, errSecretKey) } } // TestManifestRefusesTwoSignatures loads a manifest whose signature field // holds its good signature twice, not armored. Loading must refuse it. func TestManifestRefusesTwoSignatures(t *testing.T) { t.Parallel() manifest := rewriteOuter(t, signedTestManifest(t, testSigningOptions(t)), func(outer *MFFileOuter) { sig, err := dearmor(outer.GetSignature()) require.NoError(t, err) outer.Signature = slices.Concat(sig, sig) }) _, err := NewManifestFromReader(bytes.NewReader(manifest)) require.ErrorIs(t, err, errNotOneSignature) } // TestManifestRefusesFieldNotOneArmoredBlock loads manifests whose // signature or embedded public key block holds its good armored block with // something else: a second armored block, text after the END line, or many // END lines before the block. Decoding the block once for each END line // before it would take time and memory that grow with the square of the // field's size. Loading must refuse each. func TestManifestRefusesFieldNotOneArmoredBlock(t *testing.T) { t.Parallel() manifest := signedTestManifest(t, testSigningOptions(t)) for name, change := range map[string]func([]byte) []byte{ "second armored block": func(block []byte) []byte { return joinArmored(block, block) }, "text after the END line": func(block []byte) []byte { return slices.Concat(block, []byte("\nmore text\n")) }, "END lines before the block": func(block []byte) []byte { return slices.Concat( []byte(strings.Repeat(armorEnd+"\n", 1000)), block) }, } { t.Run(name, func(t *testing.T) { t.Parallel() for _, changed := range [][]byte{ rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.Signature = change(outer.GetSignature()) }), rewriteOuter(t, manifest, func(outer *MFFileOuter) { outer.SigningPubKey = change(outer.GetSigningPubKey()) }), } { _, err := NewManifestFromReader(bytes.NewReader(changed)) require.ErrorIs(t, err, errNotOneArmoredBlock) } }) } } // TestManifestSignedWithSubkey signs with a key that has a signing subkey, // which signs in place of the primary key. The manifest must load, with // the primary key's fingerprint as signer. func TestManifestSignedWithSubkey(t *testing.T) { t.Parallel() key := newTestKey(t, nil) require.NoError(t, key.AddSigningSubkey( &packet.Config{Algorithm: packet.PubKeyAlgoEdDSA})) m, err := NewManifestFromReader(bytes.NewReader(signedTestManifest(t, &SigningOptions{SecretKey: armoredSecretKeys(t, key)}))) require.NoError(t, err) assert.Equal(t, fingerprint(key), string(m.pbOuter.GetSigner())) block, err := armor.Decode(bytes.NewReader(m.pbOuter.GetSignature())) require.NoError(t, err) p, err := packet.Read(block.Body) require.NoError(t, err) sig, ok := p.(*packet.Signature) require.True(t, ok) subkey := key.Subkeys[len(key.Subkeys)-1].PublicKey assert.Equal(t, subkey.KeyId, *sig.IssuerKeyId, "the signing subkey made the signature") } // TestManifestRefusesSignerOtherThanSigningKey loads a manifest whose // signer field names a key other than the one that made the signature. func TestManifestRefusesSignerOtherThanSigningKey(t *testing.T) { t.Parallel() manifest := rewriteOuter(t, signedTestManifest(t, testSigningOptions(t)), func(outer *MFFileOuter) { outer.Signer = []byte(strings.Repeat("A", len(outer.GetSigner()))) }) _, err := NewManifestFromReader(bytes.NewReader(manifest)) require.ErrorIs(t, err, errSignerNotSigningKey) } func TestBuilderWithoutSigning(t *testing.T) { t.Parallel() // Create a builder without signing options b := NewBuilder() // Add a test file content := []byte("test file content") reader := bytes.NewReader(content) _, err := b.AddFile("test.txt", FileSize(len(content)), ModTime{}, 0, reader, nil) require.NoError(t, err) // Build the manifest var buf bytes.Buffer err = b.Build(context.Background(), &buf) require.NoError(t, err) // Parse the manifest and verify signature fields are empty manifest, err := NewManifestFromReader(&buf) require.NoError(t, err) require.NotNil(t, manifest.pbOuter) assert.Empty(t, manifest.pbOuter.GetSignature(), "signature should be empty when not signing") assert.Empty(t, manifest.pbOuter.GetSigner(), "signer should be empty when not signing") assert.Empty(t, manifest.pbOuter.GetSigningPubKey(), "signing public key should be empty when not signing") } // TestBuildPassesContextToSigning checks that Build does not sign once the // context given to it has ended. func TestBuildPassesContextToSigning(t *testing.T) { t.Parallel() b := NewBuilder() b.SetSigningOptions(&SigningOptions{SecretKey: []byte("any")}) ctx, cancel := context.WithCancel(context.Background()) cancel() require.ErrorIs(t, b.Build(ctx, io.Discard), context.Canceled) }