diff --git a/pgp/gnupg.go b/pgp/gnupg.go index 59e405f7..00ffec5a 100644 --- a/pgp/gnupg.go +++ b/pgp/gnupg.go @@ -209,6 +209,40 @@ func (g *GpgVerifier) argsKeyrings() (args []string) { return } +type gpgvSignatureDisposition int + +const ( + gpgvSignatureOther gpgvSignatureDisposition = iota + gpgvSignatureGood + gpgvSignatureUnknown + gpgvSignatureDisqualified +) + +type gpgvSignatureStatus struct { + goodSig bool + validSig bool + missingKeyErrSig bool + noPubKey bool + disqualified bool + otherFailure bool +} + +func (status gpgvSignatureStatus) disposition() gpgvSignatureDisposition { + if status.disqualified { + return gpgvSignatureDisqualified + } + if status.otherFailure { + return gpgvSignatureOther + } + if status.goodSig && status.validSig { + return gpgvSignatureGood + } + if status.missingKeyErrSig && status.noPubKey && !status.goodSig && !status.validSig { + return gpgvSignatureUnknown + } + return gpgvSignatureOther +} + func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (*KeyInfo, error) { args = append([]string{"--status-fd", "3"}, args...) cmd := exec.Command(g.gpgv, args...) @@ -255,22 +289,82 @@ func (g *GpgVerifier) runGpgv(args []string, context string, showKeyTip bool) (* statusr := bufio.NewScanner(tempf) result := &KeyInfo{} + statuses := []gpgvSignatureStatus{} + var status *gpgvSignatureStatus + flushStatus := func() { + if status != nil { + statuses = append(statuses, *status) + } + } for statusr.Scan() { line := strings.TrimSpace(statusr.Text()) + fields := strings.Fields(line) + if len(fields) >= 2 && fields[0] == "[GNUPG:]" && fields[1] == "NEWSIG" { + flushStatus() + status = &gpgvSignatureStatus{} + continue + } + if status == nil { + status = &gpgvSignatureStatus{otherFailure: true} + } + if len(fields) < 2 || fields[0] != "[GNUPG:]" { + status.otherFailure = true + continue + } - if strings.HasPrefix(line, "[GNUPG:] GOODSIG ") { - result.GoodKeys = append(result.GoodKeys, Key(strings.Fields(line)[2])) - } else if strings.HasPrefix(line, "[GNUPG:] NO_PUBKEY ") { - result.MissingKeys = append(result.MissingKeys, Key(strings.Fields(line)[2])) + switch fields[1] { + case "GOODSIG": + if len(fields) < 3 { + status.otherFailure = true + continue + } + status.goodSig = true + result.GoodKeys = append(result.GoodKeys, Key(fields[2])) + case "VALIDSIG": + status.validSig = true + case "NO_PUBKEY": + if len(fields) < 3 { + status.otherFailure = true + continue + } + status.noPubKey = true + result.MissingKeys = append(result.MissingKeys, Key(fields[2])) + case "ERRSIG": + // Field 7 is the gpg-error reason; 9 is GPG_ERR_NO_PUBKEY. + if len(fields) > 7 && fields[7] == "9" { + status.missingKeyErrSig = true + } else { + status.otherFailure = true + } + case "BADSIG", "EXPSIG", "EXPKEYSIG", "REVKEYSIG": + status.disqualified = true } } + flushStatus() if err = statusr.Err(); err != nil { return nil, err } if cmderr != nil { + goodSignatures := 0 + unknownSignatures := 0 + failureExplained := cmd.ProcessState != nil && cmd.ProcessState.Exited() + for _, signatureStatus := range statuses { + switch signatureStatus.disposition() { + case gpgvSignatureGood: + goodSignatures++ + case gpgvSignatureUnknown: + unknownSignatures++ + default: + failureExplained = false + } + } + if failureExplained && goodSignatures > 0 && unknownSignatures > 0 { + return result, nil + } + if showKeyTip && len(g.keyRings) == 0 && len(result.MissingKeys) > 0 { fmt.Printf("\nLooks like some keys are missing in your trusted keyring, you may consider importing them from keyserver:\n\n") diff --git a/pgp/gnupg_test.go b/pgp/gnupg_test.go index 086fc366..84ab2532 100644 --- a/pgp/gnupg_test.go +++ b/pgp/gnupg_test.go @@ -93,8 +93,14 @@ func (s *Gnupg1VerifierSuite) SetUpTest(c *C) { s.verifier = NewGpgVerifier(finder) s.verifier.AddKeyring("./trusted.gpg") + s.multisigOneVerifier = NewGpgVerifier(finder) + s.multisigOneVerifier.AddKeyring("./multisig-one.gpg") + s.multisigBothVerifier = NewGpgVerifier(finder) + s.multisigBothVerifier.AddKeyring("./multisig-both.gpg") c.Assert(s.verifier.InitKeyring(false), IsNil) + c.Assert(s.multisigOneVerifier.InitKeyring(false), IsNil) + c.Assert(s.multisigBothVerifier.InitKeyring(false), IsNil) } type Gnupg1SignerSuite struct { @@ -142,8 +148,191 @@ func (s *Gnupg2VerifierSuite) SetUpTest(c *C) { s.verifier = NewGpgVerifier(finder) s.verifier.AddKeyring("./trusted.gpg") + s.multisigOneVerifier = NewGpgVerifier(finder) + s.multisigOneVerifier.AddKeyring("./multisig-one.gpg") + s.multisigBothVerifier = NewGpgVerifier(finder) + s.multisigBothVerifier.AddKeyring("./multisig-both.gpg") c.Assert(s.verifier.InitKeyring(false), IsNil) + c.Assert(s.multisigOneVerifier.InitKeyring(false), IsNil) + c.Assert(s.multisigBothVerifier.InitKeyring(false), IsNil) +} + +// The status fixtures were generated with GnuPG 2.4 at fixed times so the +// expired key and subkey remain expired without making the other keys age out: +// +// export GNUPGHOME=$(mktemp -d); chmod 700 "$GNUPGHOME" +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status good fixture ' rsa2048 sign 0 +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status expired fixture ' rsa2048 sign seconds=5 +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status unknown fixture ' rsa2048 sign 0 +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly status revoked fixture ' rsa2048 sign 0 +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-generate-key 'Aptly unused expired subkey fixture ' rsa2048 cert 0 +// good=A35DB05543372EE999F6EF74EBC0B25127B9BF5A expired=DA1728275CB25D14DC8B6DC97D0DD4A295844522 unknown=A068D4E79AD454D37C0888F5B316843CAB67504E +// revoked=CD11F407D4CEB092E66A83EC402BF908BCB202A9 primary=E1745C372D24AC29BD57FAB094ABAB0251F5E2FC active=724D68416C95AA282D8280DF22EE1E88523FD531 +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-add-key "$primary" rsa2048 sign 0 +// gpg --batch --pinentry-mode loopback --passphrase '' --faked-system-time 20250101T000000 --quick-add-key "$primary" rsa2048 sign seconds=5 +// gpg --batch --yes --faked-system-time 20250101T000002 --local-user "$good!" --local-user "$expired!" --local-user "$unknown!" --detach-sign --output status-expired-held.signature multisig.text +// gpg --batch --yes --export "$good" "$expired" > status-expired-held.gpg +// gpg --batch --yes --faked-system-time 20250101T000002 --local-user "$good!" --local-user "$revoked!" --local-user "$unknown!" --detach-sign --output status-revoked-held.signature multisig.text +// sed 's/^://' "$GNUPGHOME/openpgp-revocs.d/$revoked.rev" | gpg --batch --yes --import +// gpg --batch --yes --export "$good" "$revoked" > status-revoked-held.gpg +// gpg --batch --yes --faked-system-time 20260902T143800 --local-user "$active!" --local-user "$unknown!" --detach-sign --output status-unused-expired-subkey.signature multisig.text +// gpg --batch --yes --export "$primary" > status-unused-expired-subkey.gpg + +func verifyGpgStatusFixture(c *C, finder GPGFinder, keyring, signature string) error { + verifier := NewGpgVerifier(finder) + verifier.AddKeyring(keyring) + c.Assert(verifier.InitKeyring(false), IsNil) + + cleartext, err := os.Open("multisig.text") + c.Assert(err, IsNil) + defer func() { _ = cleartext.Close() }() + + signatureFile, err := os.Open(signature) + c.Assert(err, IsNil) + defer func() { _ = signatureFile.Close() }() + + return verifier.VerifyDetachedSignature(signatureFile, cleartext, false) +} + +func (s *Gnupg2VerifierSuite) TestGpgVerifierRejectsExpiredHeldSignatureAlongsideGood(c *C) { + err := verifyGpgStatusFixture(c, GPG2Finder(), "./status-expired-held.gpg", "status-expired-held.signature") + c.Assert(err, NotNil) +} + +func (s *Gnupg2VerifierSuite) TestGpgVerifierRejectsRevokedHeldSignatureAlongsideGood(c *C) { + err := verifyGpgStatusFixture(c, GPG2Finder(), "./status-revoked-held.gpg", "status-revoked-held.signature") + c.Assert(err, NotNil) +} + +func (s *Gnupg2VerifierSuite) TestGpgVerifierIgnoresUnusedExpiredSubkey(c *C) { + err := verifyGpgStatusFixture(c, GPG2Finder(), "./status-unused-expired-subkey.gpg", "status-unused-expired-subkey.signature") + c.Assert(err, IsNil) +} + +type fixedGPGFinder struct { + gpgv string +} + +func (f fixedGPGFinder) FindGPG() (string, GPGVersion, error) { + return f.gpgv, GPG22xPlus, nil +} + +func (f fixedGPGFinder) FindGPGV() (string, GPGVersion, error) { + return f.gpgv, GPG22xPlus, nil +} + +func (s *GnupgSuite) TestGpgVerifierRejectsUnexplainedFailure(c *C) { + tests := []struct { + name string + body string + wantError bool + goodKeys []Key + missingKeys []Key + }{ + { + name: "unexplained exit", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] GOODSIG 0123456789ABCDEF held' \ + '[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' >&3 +exit 70 +`, + wantError: true, + goodKeys: []Key{"0123456789ABCDEF"}, + }, + { + name: "unknown co-signer", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] GOODSIG 0123456789ABCDEF held' \ + '[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] ERRSIG FEDCBA9876543210 1 10 00 0 9 0000000000000000FEDCBA9876543210' \ + '[GNUPG:] NO_PUBKEY FEDCBA9876543210' >&3 +exit 2 +`, + goodKeys: []Key{"0123456789ABCDEF"}, + missingKeys: []Key{"FEDCBA9876543210"}, + }, + { + name: "non-missing-key ERRSIG", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] GOODSIG 0123456789ABCDEF held' \ + '[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] ERRSIG FEDCBA9876543210 1 10 00 0 1 0000000000000000FEDCBA9876543210' >&3 +exit 2 +`, + wantError: true, + goodKeys: []Key{"0123456789ABCDEF"}, + }, + { + name: "empty signature block", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] GOODSIG 0123456789ABCDEF held' \ + '[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' \ + '[GNUPG:] NEWSIG' >&3 +exit 2 +`, + wantError: true, + goodKeys: []Key{"0123456789ABCDEF"}, + }, + { + name: "GOODSIG without VALIDSIG", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] GOODSIG 0123456789ABCDEF held' >&3 +exit 2 +`, + wantError: true, + goodKeys: []Key{"0123456789ABCDEF"}, + }, + { + name: "signal death", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] GOODSIG 0123456789ABCDEF held' \ + '[GNUPG:] VALIDSIG 00000000000000000123456789ABCDEF 0 0 0 0 0 0 0 00 00000000000000000123456789ABCDEF' >&3 +kill -TERM "$$" +`, + wantError: true, + goodKeys: []Key{"0123456789ABCDEF"}, + }, + { + name: "exit zero preserves upstream behavior", + body: `#!/bin/sh +printf '%s\n' \ + '[GNUPG:] NEWSIG' \ + '[GNUPG:] BADSIG 0123456789ABCDEF held' >&3 +exit 0 +`, + }, + } + + for _, test := range tests { + directory := c.MkDir() + gpgv := filepath.Join(directory, "gpgv") + c.Assert(os.WriteFile(gpgv, []byte(test.body), 0o700), IsNil) + + verifier := NewGpgVerifier(fixedGPGFinder{gpgv: gpgv}) + keyInfo, err := verifier.runGpgv(nil, "detached signature", false) + if test.wantError { + c.Check(err, NotNil, Commentf(test.name)) + } else { + c.Check(err, IsNil, Commentf(test.name)) + } + c.Check(keyInfo.GoodKeys, DeepEquals, test.goodKeys, Commentf(test.name)) + c.Check(keyInfo.MissingKeys, DeepEquals, test.missingKeys, Commentf(test.name)) + } } type Gnupg2SignerSuite struct { diff --git a/pgp/internal_test.go b/pgp/internal_test.go index 8601179f..f3b2f73a 100644 --- a/pgp/internal_test.go +++ b/pgp/internal_test.go @@ -13,8 +13,14 @@ var _ = Suite(&GoVerifierSuite{}) func (s *GoVerifierSuite) SetUpTest(c *C) { s.verifier = &GoVerifier{} s.verifier.AddKeyring("./trusted.gpg") + s.multisigOneVerifier = &GoVerifier{} + s.multisigOneVerifier.AddKeyring("./multisig-one.gpg") + s.multisigBothVerifier = &GoVerifier{} + s.multisigBothVerifier.AddKeyring("./multisig-both.gpg") c.Assert(s.verifier.InitKeyring(false), IsNil) + c.Assert(s.multisigOneVerifier.InitKeyring(false), IsNil) + c.Assert(s.multisigBothVerifier.InitKeyring(false), IsNil) } type GoSignerSuite struct { diff --git a/pgp/multisig-bad.text b/pgp/multisig-bad.text new file mode 100644 index 00000000..0684cb08 --- /dev/null +++ b/pgp/multisig-bad.text @@ -0,0 +1 @@ +tampered aptly multi-signature verification fixture diff --git a/pgp/multisig-both.gpg b/pgp/multisig-both.gpg new file mode 100644 index 00000000..1cb44a82 Binary files /dev/null and b/pgp/multisig-both.gpg differ diff --git a/pgp/multisig-known-bad.signature b/pgp/multisig-known-bad.signature new file mode 100644 index 00000000..f763c2cd Binary files /dev/null and b/pgp/multisig-known-bad.signature differ diff --git a/pgp/multisig-known-good.signature b/pgp/multisig-known-good.signature new file mode 100644 index 00000000..e025ea04 Binary files /dev/null and b/pgp/multisig-known-good.signature differ diff --git a/pgp/multisig-one.gpg b/pgp/multisig-one.gpg new file mode 100644 index 00000000..c0604445 Binary files /dev/null and b/pgp/multisig-one.gpg differ diff --git a/pgp/multisig.clearsigned b/pgp/multisig.clearsigned new file mode 100644 index 00000000..f94c9e3d --- /dev/null +++ b/pgp/multisig.clearsigned @@ -0,0 +1,21 @@ +-----BEGIN PGP SIGNED MESSAGE----- +Hash: SHA512 + +aptly multi-signature verification fixture +-----BEGIN PGP SIGNATURE----- + +iQEzBAEBCgAdFiEEqYoHYeLwl1cZL9VhhT6Gnff0SYYFAmrmaZIACgkQhT6Gnff0 +SYYKYAgAujy2QKb0Gjg9YSX16Ff5XCkd/XJSVwSHxVfX5TXN2daby+a3fns78Z9u +gbNF8qB0n6P8Q8xpPvrvnx125yJidutBR77Y1fjBm5Ks6N/x1kWg9lIXwS5eYglA +I7Ctm3vEdD96wyYnHiBOo9sufasrfX1CnCBd1q1MkDrNpaiYmvSMeK+f6Ozs7OK2 +IfYNB5dNsI/LI65bKbgGFiOXoqMlXYFDn9GA734jrk3CvOImks4Q9GEi/qhaPKHe +sU9lZ9KU9Okj9Yy96c7Dk29iGaxhhKTiZ+2UGvyczVRenf2+v5t8/M11CgagApPP +nq8MRgPBKk6BdMgMd0SyZZhPj0VEpokBMwQBAQoAHRYhBOyPYRMwLkNvKOuL0hPZ +BG62qCWlBQJq5mmSAAoJEBPZBG62qCWlgv8H/1CEchjYZDgPWFu7cZE+abBPByTX +VcYU62CQPBpkxJ00pviuKgYRzQX0FCxQxpQOQlHv3ToVzL3a62m1vrTRa3NhT0U8 +DYdRxcW8o/11hnY3VeiPavOvORhwn70FeBYtYw7BipHHxwOElh7Ey5dkzHS+Z2OL +0kCNvltjKEFZM+q4w2yPbSpam7wCS5gYLl0d+99ornPtzDwBOreXFoC1MER3y1hB +H3XbLqggE6gfumfiGR9Hljvh5CT/ebk7RwMWCcrp3/MdXwfpH+6JGg1AvF/TwmEi +XAl8LljwTybylH+8wKAHYX4ed7GvJBOigJth+3o7sh8n7oqL0YJH0JoSjbM= +=ThV5 +-----END PGP SIGNATURE----- diff --git a/pgp/multisig.signature b/pgp/multisig.signature new file mode 100644 index 00000000..21f33aad Binary files /dev/null and b/pgp/multisig.signature differ diff --git a/pgp/multisig.text b/pgp/multisig.text new file mode 100644 index 00000000..1e836404 --- /dev/null +++ b/pgp/multisig.text @@ -0,0 +1 @@ +aptly multi-signature verification fixture diff --git a/pgp/openpgp.go b/pgp/openpgp.go index 335a9ad9..284d01ad 100644 --- a/pgp/openpgp.go +++ b/pgp/openpgp.go @@ -63,7 +63,9 @@ func checkDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader for { p, err = packets.Next() if err == io.EOF { - if len(signers) == 0 || missingKeys > 0 { + // Revocation and expiry of held keys deliberately remain unchanged + // from upstream in this provider; only unknown co-signers are ignored. + if len(signers) == missingKeys { err = errors.ErrUnknownIssuer } else { err = nil diff --git a/pgp/status-expired-held.gpg b/pgp/status-expired-held.gpg new file mode 100644 index 00000000..c35f2cb9 Binary files /dev/null and b/pgp/status-expired-held.gpg differ diff --git a/pgp/status-expired-held.signature b/pgp/status-expired-held.signature new file mode 100644 index 00000000..5ea5015d Binary files /dev/null and b/pgp/status-expired-held.signature differ diff --git a/pgp/status-revoked-held.gpg b/pgp/status-revoked-held.gpg new file mode 100644 index 00000000..631ad221 Binary files /dev/null and b/pgp/status-revoked-held.gpg differ diff --git a/pgp/status-revoked-held.signature b/pgp/status-revoked-held.signature new file mode 100644 index 00000000..806d5e2a Binary files /dev/null and b/pgp/status-revoked-held.signature differ diff --git a/pgp/status-unused-expired-subkey.gpg b/pgp/status-unused-expired-subkey.gpg new file mode 100644 index 00000000..34fcc5aa Binary files /dev/null and b/pgp/status-unused-expired-subkey.gpg differ diff --git a/pgp/status-unused-expired-subkey.signature b/pgp/status-unused-expired-subkey.signature new file mode 100644 index 00000000..d5c5ca12 Binary files /dev/null and b/pgp/status-unused-expired-subkey.signature differ diff --git a/pgp/verify_test.go b/pgp/verify_test.go index 89fdbc14..5529df4c 100644 --- a/pgp/verify_test.go +++ b/pgp/verify_test.go @@ -10,9 +10,33 @@ import ( // Common set of tests shared by internal & external GnuPG implementations type VerifierSuite struct { - verifier Verifier + verifier Verifier + multisigOneVerifier Verifier + multisigBothVerifier Verifier } +// The partial-keyring fixtures were generated with GnuPG 2.4 at a fixed time +// so they remain valid under the CI faketime window (2025-01-02): +// +// gpg --batch --pinentry-mode loopback --passphrase '' \ +// --faked-system-time 20250101T000000 \ +// --quick-generate-key 'Aptly multisign known fixture ' rsa2048 sign 0 +// gpg --batch --pinentry-mode loopback --passphrase '' \ +// --faked-system-time 20250101T000000 \ +// --quick-generate-key 'Aptly multisign unknown fixture ' rsa2048 sign 0 +// gpg --export > multisig-both.gpg +// gpg --export > multisig-one.gpg +// gpg --local-user --local-user \ +// --detach-sign --output multisig.signature multisig.text +// gpg --local-user --local-user \ +// --clearsign --output multisig.clearsigned multisig.text +// gpg --local-user --detach-sign --output multisig-known-good.signature multisig.text +// gpg --local-user --detach-sign --output multisig-known-bad.signature multisig-bad.text +// +// Replace and with the actual fingerprints from step 2. +// +// multisig-bad.text was made by changing multisig.text after signing it. + func (s *VerifierSuite) TestVerifyDetached(c *C) { for _, test := range []struct { textName, signatureName string @@ -37,21 +61,82 @@ func (s *VerifierSuite) TestVerifyDetached(c *C) { } func (s *VerifierSuite) TestVerifyClearsigned(c *C) { - for _, test := range []struct { - clearSignedName string - }{ - {"1.clearsigned"}, - } { - clearsigned, err := os.Open(test.clearSignedName) - c.Assert(err, IsNil) + clearsigned, err := os.Open("multisig.clearsigned") + c.Assert(err, IsNil) + defer func() { _ = clearsigned.Close() }() - keyInfo, err := s.verifier.VerifyClearsigned(clearsigned, false) - c.Assert(err, IsNil) - c.Check(keyInfo.GoodKeys, DeepEquals, []Key{"04EE7237B7D453EC", "648ACFD622F3D138", "DCC9EFBF77E11517"}) - c.Check(keyInfo.MissingKeys, DeepEquals, []Key(nil)) + keyInfo, err := s.multisigBothVerifier.VerifyClearsigned(clearsigned, false) + c.Assert(err, IsNil) + c.Check(keyInfo.GoodKeys, DeepEquals, []Key{"853E869DF7F44986", "13D9046EB6A825A5"}) + c.Check(keyInfo.MissingKeys, DeepEquals, []Key(nil)) +} - _ = clearsigned.Close() - } +func (s *VerifierSuite) TestVerifyClearsignedPartialKeyring(c *C) { + clearsigned, err := os.Open("multisig.clearsigned") + c.Assert(err, IsNil) + defer func() { _ = clearsigned.Close() }() + + keyInfo, err := s.multisigOneVerifier.VerifyClearsigned(clearsigned, false) + c.Assert(err, IsNil) + c.Check(keyInfo.GoodKeys, DeepEquals, []Key{"853E869DF7F44986"}) + c.Check(keyInfo.MissingKeys, DeepEquals, []Key{"13D9046EB6A825A5"}) +} + +func (s *VerifierSuite) TestVerifyDetachedPartialKeyring(c *C) { + cleartext, err := os.Open("multisig.text") + c.Assert(err, IsNil) + defer func() { _ = cleartext.Close() }() + + signature, err := os.Open("multisig.signature") + c.Assert(err, IsNil) + defer func() { _ = signature.Close() }() + + err = s.multisigOneVerifier.VerifyDetachedSignature(signature, cleartext, false) + c.Assert(err, IsNil) +} + +func (s *VerifierSuite) TestVerifyPartialKeyringRejectsBadSignature(c *C) { + cleartext, err := os.Open("multisig-bad.text") + c.Assert(err, IsNil) + defer func() { _ = cleartext.Close() }() + + signature, err := os.Open("multisig.signature") + c.Assert(err, IsNil) + defer func() { _ = signature.Close() }() + + err = s.multisigOneVerifier.VerifyDetachedSignature(signature, cleartext, false) + c.Assert(err, NotNil) +} + +func (s *VerifierSuite) TestVerifyRejectsBadSignatureAlongsideGood(c *C) { + cleartext, err := os.Open("multisig.text") + c.Assert(err, IsNil) + defer func() { _ = cleartext.Close() }() + + goodSignature, err := os.Open("multisig-known-good.signature") + c.Assert(err, IsNil) + defer func() { _ = goodSignature.Close() }() + + badSignature, err := os.Open("multisig-known-bad.signature") + c.Assert(err, IsNil) + defer func() { _ = badSignature.Close() }() + + signature := io.MultiReader(goodSignature, badSignature) + err = s.multisigBothVerifier.VerifyDetachedSignature(signature, cleartext, false) + c.Assert(err, NotNil) +} + +func (s *VerifierSuite) TestVerifyNoKnownSignerStillFails(c *C) { + cleartext, err := os.Open("multisig.text") + c.Assert(err, IsNil) + defer func() { _ = cleartext.Close() }() + + signature, err := os.Open("multisig.signature") + c.Assert(err, IsNil) + defer func() { _ = signature.Close() }() + + err = s.verifier.VerifyDetachedSignature(signature, cleartext, false) + c.Assert(err, NotNil) } func (s *VerifierSuite) TestExtractClearsigned(c *C) {