Files
aptly/pgp/openpgp.go
T
Daniel Pinder 59ff63de62 pgp: accept multi-signed releases when the keyring holds only some signers
Debian co-signs each suite's InRelease with more than one archive key, so
gpgv reports a good signature from the key you trust and NO_PUBKEY for the
others, exiting 2. Both verifiers treat that as failure, which means
mirroring Debian requires importing every key Debian happens to sign with.
apt does not require this: it is fatal only when there is no good signature
or a BADSIG.

Adopt that rule for unknown signers. When gpgv exits non-zero the status
stream is parsed per NEWSIG block and the result is a success only when the
process exited, at least one block is a good signature from a held key, at
least one block is an unknown issuer (ERRSIG reason 9 with NO_PUBKEY), and
nothing else appears. BADSIG, EXPSIG, EXPKEYSIG and REVKEYSIG from a held
key still fail, as does any unexplained failure. The exit-zero path is
unchanged.

KEYEXPIRED and KEYREVOKED are deliberately not disqualifiers: doc/DETAILS
notes KEYEXPIRED is emitted for an expired subkey even when that subkey was
not used, so treating it as a verdict rejects valid signatures.

checkDetachedSignature gets the equivalent change for the internal verifier.
2026-11-01 13:26:36 +01:00

240 lines
6.1 KiB
Go

package pgp
import (
"bytes"
"crypto"
"crypto/dsa" //nolint:staticcheck
"crypto/ecdsa"
"crypto/rsa"
"hash"
"io"
"strconv"
"time"
"github.com/ProtonMail/go-crypto/openpgp"
"github.com/ProtonMail/go-crypto/openpgp/armor"
"github.com/ProtonMail/go-crypto/openpgp/errors"
"github.com/ProtonMail/go-crypto/openpgp/packet"
)
// hashForSignature returns a pair of hashes that can be used to verify a
// signature. The signature may specify that the contents of the signed message
// should be preprocessed (i.e. to normalize line endings). Thus this function
// returns two hashes. The second should be used to hash the message itself and
// performs any needed preprocessing.
func hashForSignature(hashID crypto.Hash, sigType packet.SignatureType) (hash.Hash, hash.Hash, error) {
if !hashID.Available() {
return nil, nil, errors.UnsupportedError("hash not available: " + strconv.Itoa(int(hashID)))
}
h := hashID.New()
switch sigType {
case packet.SigTypeBinary:
return h, h, nil
case packet.SigTypeText:
return h, openpgp.NewCanonicalTextHash(h), nil
}
return nil, nil, errors.UnsupportedError("unsupported signature type: " + strconv.Itoa(int(sigType)))
}
type signatureResult struct {
CreationTime time.Time
IssuerKeyID uint64
PubKeyAlgo packet.PublicKeyAlgorithm
Entity *openpgp.Entity
}
// checkDetachedSignature takes a signed file and a detached signature and
// returns the signer if the signature is valid. If the signer isn't known,
// ErrUnknownIssuer is returned.
//
// This is extended version of golang.org/x/crypto/openpgp to support multiple signers and returns multiple
// signers, plus keeps track of "missing" keys
func checkDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader) (signers []signatureResult, missingKeys int, err error) {
var p packet.Packet
signedBuf := &bytes.Buffer{}
if _, e := io.Copy(signedBuf, signed); e != nil && e != io.EOF {
return nil, 0, e
}
packets := packet.NewReader(signature)
for {
p, err = packets.Next()
if err == io.EOF {
// 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
}
return
}
if err != nil {
return nil, 0, err
}
var issuerKeyID uint64
var hashFunc crypto.Hash
var sigType packet.SignatureType
var creationTime time.Time
var pubKeyAlgo packet.PublicKeyAlgorithm
var keys []openpgp.Key
switch sig := p.(type) {
case *packet.Signature:
if sig.IssuerKeyId == nil {
return nil, 0, errors.StructuralError("signature doesn't have an issuer")
}
issuerKeyID = *sig.IssuerKeyId
hashFunc = sig.Hash
sigType = sig.SigType
creationTime = sig.CreationTime
pubKeyAlgo = sig.PubKeyAlgo
default:
return nil, 0, errors.StructuralError("non signature packet found")
}
keys = keyring.KeysByIdUsage(issuerKeyID, packet.KeyFlagSign)
if len(keys) == 0 {
signers = append(signers, signatureResult{
CreationTime: creationTime,
IssuerKeyID: issuerKeyID,
PubKeyAlgo: pubKeyAlgo,
})
missingKeys++
continue
}
h, wrappedHash, err := hashForSignature(hashFunc, sigType)
if err != nil {
return nil, 0, err
}
if _, e := io.Copy(wrappedHash, bytes.NewReader(signedBuf.Bytes())); e != nil && e != io.EOF {
return nil, 0, e
}
allFailed := true
for _, key := range keys {
switch sig := p.(type) {
case *packet.Signature:
err = key.PublicKey.VerifySignature(h, sig)
default:
panic("unreachable")
}
if err == nil {
signers = append(signers, signatureResult{
CreationTime: creationTime,
IssuerKeyID: issuerKeyID,
PubKeyAlgo: pubKeyAlgo,
Entity: key.Entity,
})
allFailed = false
}
}
if allFailed {
return nil, 0, err
}
}
}
// readArmored reads an armored block with the given type.
func readArmored(r io.Reader, expectedType string) (body io.Reader, err error) {
block, err := armor.Decode(r)
if err != nil {
return
}
if block.Type != expectedType {
return nil, errors.InvalidArgumentError("expected '" + expectedType + "', got: " + block.Type)
}
return block.Body, nil
}
// checkArmoredDetachedSignature performs the same actions as
// CheckDetachedSignature but expects the signature to be armored.
//
// This is extended version of golang.org/x/crypto/openpgp to support multiple signers and returns multiple
// signers, plus keeps track of "missing" keys
func checkArmoredDetachedSignature(keyring openpgp.KeyRing, signed, signature io.Reader) (signers []signatureResult, missingKeys int, err error) {
body, err := readArmored(signature, openpgp.SignatureType)
if err != nil {
return
}
return checkDetachedSignature(keyring, signed, body)
}
func pubkeyAlgorithmName(algorithm packet.PublicKeyAlgorithm) string {
switch algorithm {
case packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSAEncryptOnly, packet.PubKeyAlgoRSASignOnly:
return "RSA"
case packet.PubKeyAlgoElGamal:
return "ElGamal"
case packet.PubKeyAlgoDSA:
return "DSA"
case packet.PubKeyAlgoECDH:
return "EDCH"
case packet.PubKeyAlgoECDSA:
return "ECDSA"
case packet.PubKeyAlgoEdDSA:
return "EdDSA"
}
return "unknown"
}
func keyBits(key interface{}) string {
switch k := key.(type) {
case *rsa.PublicKey:
return strconv.Itoa(k.N.BitLen())
case *dsa.PublicKey:
return strconv.Itoa(k.P.BitLen())
case *ecdsa.PublicKey:
return strconv.Itoa(k.Curve.Params().BitSize)
default:
return "?"
}
}
func validEntity(entity *openpgp.Entity) bool {
var selfSig *packet.Signature
for _, ident := range entity.Identities {
if selfSig == nil {
selfSig = ident.SelfSignature
} else if ident.SelfSignature.IsPrimaryId != nil && *ident.SelfSignature.IsPrimaryId {
selfSig = ident.SelfSignature
break
}
}
if selfSig == nil {
return false
}
if len(entity.Revocations) > 0 {
return false
}
if selfSig.RevocationReason != nil {
return false
}
if !selfSig.FlagsValid {
return false
}
if selfSig.KeyLifetimeSecs != nil && selfSig.CreationTime.Add(time.Duration(*selfSig.KeyLifetimeSecs)*time.Second).Before(time.Now()) {
return false
}
return true
}