mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-19 17:59:05 -05:00
622 lines
23 KiB
Go
622 lines
23 KiB
Go
// Copyright (c) The OpenTofu Authors
|
||
// SPDX-License-Identifier: MPL-2.0
|
||
// Copyright (c) 2023 HashiCorp, Inc.
|
||
// SPDX-License-Identifier: MPL-2.0
|
||
|
||
package getproviders
|
||
|
||
import (
|
||
"bufio"
|
||
"bytes"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"errors"
|
||
"fmt"
|
||
"iter"
|
||
"log"
|
||
"os"
|
||
"strings"
|
||
|
||
"github.com/ProtonMail/go-crypto/openpgp"
|
||
openpgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors"
|
||
"github.com/opentofu/opentofu/internal/addrs"
|
||
"github.com/opentofu/opentofu/internal/collections"
|
||
)
|
||
|
||
type packageAuthenticationResult int
|
||
|
||
const (
|
||
unauthenticated packageAuthenticationResult = iota
|
||
verifiedChecksum
|
||
signed
|
||
signingSkipped
|
||
)
|
||
|
||
const (
|
||
enforceGPGValidationEnvName = "OPENTOFU_ENFORCE_GPG_VALIDATION"
|
||
enforceGPGExpirationEnvName = "OPENTOFU_ENFORCE_GPG_EXPIRATION"
|
||
)
|
||
|
||
// PackageAuthenticationResult is returned from a PackageAuthentication
|
||
// implementation. It is a mostly-opaque type intended for use in UI, which
|
||
// implements Stringer.
|
||
//
|
||
// A failed PackageAuthentication attempt will return an "unauthenticated"
|
||
// result, which is represented by nil.
|
||
type PackageAuthenticationResult struct {
|
||
hashes HashDispositions
|
||
}
|
||
|
||
// NewPackageAuthenticationResult constructs a new [PackageAuthenticationResult]
|
||
// based on the given hash dispositions.
|
||
//
|
||
// This is here primarily to allow constructing expected result values for tests
|
||
// in other packages. There isn't really any reason for non-test code outside
|
||
// of this package to directly construct package authentication results, since
|
||
// all of the "real" package authentication implementations should live in this
|
||
// package.
|
||
func NewPackageAuthenticationResult(hashes HashDispositions) *PackageAuthenticationResult {
|
||
return &PackageAuthenticationResult{hashes}
|
||
}
|
||
|
||
func (t *PackageAuthenticationResult) summaryResult() packageAuthenticationResult {
|
||
if t == nil {
|
||
return unauthenticated
|
||
}
|
||
signedCount := 0
|
||
registryReportCount := 0
|
||
locallyVerifiedCount := 0
|
||
for _, disp := range t.hashes {
|
||
if disp.SignedByAnyGPGKeys() {
|
||
signedCount++
|
||
}
|
||
if disp.ReportedByRegistry {
|
||
registryReportCount++
|
||
}
|
||
if disp.VerifiedLocally {
|
||
locallyVerifiedCount++
|
||
}
|
||
}
|
||
switch {
|
||
case signedCount > 0:
|
||
return signed
|
||
case registryReportCount > 0:
|
||
return signingSkipped
|
||
case locallyVerifiedCount > 0:
|
||
return verifiedChecksum
|
||
default:
|
||
return unauthenticated
|
||
}
|
||
}
|
||
|
||
func (t *PackageAuthenticationResult) String() string {
|
||
return map[packageAuthenticationResult]string{
|
||
unauthenticated: "unauthenticated",
|
||
verifiedChecksum: "verified checksum",
|
||
signingSkipped: "signing skipped",
|
||
signed: "signed",
|
||
}[t.summaryResult()]
|
||
}
|
||
|
||
// HashesWithDisposition returns a sequence of hashes whose disposition after
|
||
// authentication matches the rule implemented by the given function cond.
|
||
//
|
||
// Use this to select the appropriate subset of hashes to record for the
|
||
// associated provider version in the dependency lock file, with the selection
|
||
// condition varying based on the authentication result and the current
|
||
// policy for whether signature verification is required and which keys
|
||
// are trusted.
|
||
func (t *PackageAuthenticationResult) HashesWithDisposition(cond func(*HashDisposition) bool) iter.Seq[Hash] {
|
||
if t == nil {
|
||
// A nil result has no hashes at all
|
||
return func(yield func(Hash) bool) {}
|
||
}
|
||
return func(yield func(Hash) bool) {
|
||
for hash, disp := range t.hashes {
|
||
if !cond(disp) {
|
||
continue
|
||
}
|
||
if keepGoing := yield(hash); !keepGoing {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// GPGKeyIDsString returns a UI-oriented string representation of all of the
|
||
// GPG key IDs that asserted the validity of at least one of the hashes
|
||
// related to this package's provider version.
|
||
func (t *PackageAuthenticationResult) GPGKeyIDsString() string {
|
||
if t == nil {
|
||
return ""
|
||
}
|
||
return t.hashes.AllGPGSigningKeysString()
|
||
}
|
||
|
||
// Signed returns whether the package was authenticated as signed by anyone.
|
||
func (t *PackageAuthenticationResult) Signed() bool {
|
||
if t == nil {
|
||
return false
|
||
}
|
||
return t.hashes.HasAnySignedByGPGKeys()
|
||
}
|
||
|
||
// SigningSkipped returns whether the package was authenticated but the key
|
||
// validation was skipped.
|
||
func (t *PackageAuthenticationResult) SigningSkipped() bool {
|
||
if t == nil || t.Signed() {
|
||
return false
|
||
}
|
||
return t.hashes.HasAnyReportedByRegistry()
|
||
}
|
||
|
||
// SigningKey represents a key used to sign packages from a registry. These are
|
||
// both in ASCII armored OpenPGP format.
|
||
//
|
||
// The JSON struct tags represent the field names used by the Registry API.
|
||
type SigningKey struct {
|
||
ASCIIArmor string `json:"ascii_armor"`
|
||
}
|
||
|
||
// PackageAuthentication is an interface implemented by the optional package
|
||
// authentication implementations a source may include on its PackageMeta
|
||
// objects.
|
||
//
|
||
// A PackageAuthentication implementation is responsible for authenticating
|
||
// that a package is what its distributor intended to distribute and that it
|
||
// has not been tampered with.
|
||
type PackageAuthentication interface {
|
||
// AuthenticatePackage takes the local location of a package (which may or
|
||
// may not be the same as the original source location), and returns a
|
||
// PackageAuthenticationResult, or an error if the authentication checks
|
||
// fail.
|
||
//
|
||
// The local location is guaranteed not to be a PackageHTTPURL: a remote
|
||
// package will always be staged locally for inspection first.
|
||
AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error)
|
||
}
|
||
|
||
type packageAuthenticationAll []PackageAuthentication
|
||
|
||
// PackageAuthenticationAll combines several authentications together into a
|
||
// single check value, which passes only if all of the given ones pass.
|
||
//
|
||
// The checks are processed in the order given, so a failure of an earlier
|
||
// check will prevent execution of a later one.
|
||
//
|
||
// The returned result is the union of the results of all authentications,
|
||
// describing all of the checksums that were somehow involved in the
|
||
// authentication process and what we learned about each one along the way.
|
||
func PackageAuthenticationAll(checks ...PackageAuthentication) PackageAuthentication {
|
||
return packageAuthenticationAll(checks)
|
||
}
|
||
|
||
func (checks packageAuthenticationAll) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||
authResult := &PackageAuthenticationResult{
|
||
hashes: make(HashDispositions),
|
||
}
|
||
for _, check := range checks {
|
||
thisAuthResult, err := check.AuthenticatePackage(localLocation)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if thisAuthResult == nil {
|
||
continue // this result has nothing to contribute to our overall result
|
||
}
|
||
authResult.hashes.Merge(thisAuthResult.hashes)
|
||
}
|
||
return authResult, nil
|
||
}
|
||
|
||
type packageHashAuthentication struct {
|
||
RequiredHashes []Hash
|
||
AllHashes []Hash
|
||
Platform Platform
|
||
}
|
||
|
||
// NewPackageHashAuthentication returns a PackageAuthentication implementation
|
||
// that checks whether the contents of the package match whatever subset of the
|
||
// given hashes are considered acceptable by the current version of OpenTofu.
|
||
//
|
||
// This uses the hash algorithms implemented by functions PackageHash and
|
||
// MatchesHash. The PreferredHashes function will select which of the given
|
||
// hashes are considered by OpenTofu to be the strongest verification, and
|
||
// authentication succeeds as long as one of those matches.
|
||
func NewPackageHashAuthentication(platform Platform, validHashes []Hash) PackageAuthentication {
|
||
requiredHashes := PreferredHashes(validHashes)
|
||
return packageHashAuthentication{
|
||
RequiredHashes: requiredHashes,
|
||
AllHashes: validHashes,
|
||
Platform: platform,
|
||
}
|
||
}
|
||
|
||
func (a packageHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||
if len(a.RequiredHashes) == 0 {
|
||
// Indicates that none of the hashes given to
|
||
// NewPackageHashAuthentication were considered to be usable by this
|
||
// version of OpenTofu.
|
||
return nil, fmt.Errorf("this version of OpenTofu does not support any of the checksum formats given for this provider")
|
||
}
|
||
|
||
hashes := make(HashDispositions, len(a.RequiredHashes))
|
||
for verifiedHash, err := range HashesMatchingPackage(localLocation, a.RequiredHashes) {
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to verify provider package checksums: %w", err)
|
||
}
|
||
hashes[verifiedHash] = &HashDisposition{
|
||
VerifiedLocally: true,
|
||
}
|
||
}
|
||
|
||
if len(hashes) > 0 {
|
||
return &PackageAuthenticationResult{hashes: hashes}, nil
|
||
}
|
||
if len(a.RequiredHashes) == 1 {
|
||
return nil, fmt.Errorf("provider package doesn't match the expected checksum %q", a.RequiredHashes[0].String())
|
||
}
|
||
// It's non-ideal that this doesn't actually list the expected checksums,
|
||
// but in the many-checksum case the message would get pretty unwieldy.
|
||
// In practice today we typically use this authenticator only with a
|
||
// single hash returned from a network mirror, so the better message
|
||
// above will prevail in that case. Maybe we'll improve on this somehow
|
||
// if the future introduction of a new hash scheme causes there to more
|
||
// commonly be multiple hashes.
|
||
return nil, fmt.Errorf("provider package doesn't match the any of the expected checksums")
|
||
}
|
||
|
||
type archiveHashAuthentication struct {
|
||
Platform Platform
|
||
WantSHA256Sum [sha256.Size]byte
|
||
}
|
||
|
||
// NewArchiveChecksumAuthentication returns a PackageAuthentication
|
||
// implementation that checks that the original distribution archive matches
|
||
// the given hash.
|
||
//
|
||
// This authentication is suitable only for PackageHTTPURL and
|
||
// PackageLocalArchive source locations, because the unpacked layout
|
||
// (represented by PackageLocalDir) does not retain access to the original
|
||
// source archive. Therefore this authenticator will return an error if its
|
||
// given localLocation is not PackageLocalArchive.
|
||
//
|
||
// NewPackageHashAuthentication is preferable to use when possible because
|
||
// it uses the newer hashing scheme (implemented by function PackageHash) that
|
||
// can work with both packed and unpacked provider packages.
|
||
func NewArchiveChecksumAuthentication(platform Platform, wantSHA256Sum [sha256.Size]byte) PackageAuthentication {
|
||
return archiveHashAuthentication{platform, wantSHA256Sum}
|
||
}
|
||
|
||
func (a archiveHashAuthentication) AuthenticatePackage(localLocation PackageLocation) (*PackageAuthenticationResult, error) {
|
||
archiveLocation, ok := localLocation.(PackageLocalArchive)
|
||
if !ok {
|
||
// A source should not use this authentication type for non-archive
|
||
// locations.
|
||
return nil, fmt.Errorf("cannot check archive hash for non-archive location %s", localLocation)
|
||
}
|
||
|
||
gotHash, err := PackageHashLegacyZipSHA(archiveLocation)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to compute checksum for %s: %w", archiveLocation, err)
|
||
}
|
||
wantHash := HashLegacyZipSHAFromSHA(a.WantSHA256Sum)
|
||
if gotHash != wantHash {
|
||
return nil, fmt.Errorf("archive has incorrect checksum %s (expected %s)", gotHash, wantHash)
|
||
}
|
||
return &PackageAuthenticationResult{
|
||
hashes: HashDispositions{
|
||
gotHash: &HashDisposition{
|
||
VerifiedLocally: true,
|
||
},
|
||
},
|
||
}, nil
|
||
}
|
||
|
||
type matchingChecksumAuthentication struct {
|
||
Document []byte
|
||
Filename string
|
||
WantSHA256Sum [sha256.Size]byte
|
||
}
|
||
|
||
// NewMatchingChecksumAuthentication returns a PackageAuthentication
|
||
// implementation that scans a registry-provided SHA256SUMS document for a
|
||
// specified filename, and compares the SHA256 hash against the expected hash.
|
||
// This is necessary to ensure that the signed SHA256SUMS document matches the
|
||
// declared SHA256 hash for the package, and therefore that a valid signature
|
||
// of this document authenticates the package.
|
||
//
|
||
// This authentication always returns a nil result, since it alone cannot offer
|
||
// any assertions about package integrity. It should be combined with other
|
||
// authentications to be useful.
|
||
func NewMatchingChecksumAuthentication(document []byte, filename string, wantSHA256Sum [sha256.Size]byte) PackageAuthentication {
|
||
return matchingChecksumAuthentication{
|
||
Document: document,
|
||
Filename: filename,
|
||
WantSHA256Sum: wantSHA256Sum,
|
||
}
|
||
}
|
||
|
||
func (m matchingChecksumAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) {
|
||
// Find the checksum in the list with matching filename. The document is
|
||
// in the form "0123456789abcdef filename.zip".
|
||
filename := []byte(m.Filename)
|
||
var checksum []byte
|
||
for _, line := range bytes.Split(m.Document, []byte("\n")) {
|
||
parts := bytes.Fields(line)
|
||
if len(parts) > 1 && bytes.Equal(parts[1], filename) {
|
||
checksum = parts[0]
|
||
break
|
||
}
|
||
}
|
||
if checksum == nil {
|
||
return nil, fmt.Errorf("checksum list has no SHA-256 hash for %q", m.Filename)
|
||
}
|
||
|
||
// Decode the ASCII checksum into a byte array for comparison.
|
||
var gotSHA256Sum [sha256.Size]byte
|
||
if _, err := hex.Decode(gotSHA256Sum[:], checksum); err != nil {
|
||
return nil, fmt.Errorf("checksum list has invalid SHA256 hash %q: %w", string(checksum), err)
|
||
}
|
||
|
||
// If the checksums don't match, authentication fails.
|
||
if !bytes.Equal(gotSHA256Sum[:], m.WantSHA256Sum[:]) {
|
||
return nil, fmt.Errorf("checksum list has unexpected SHA-256 hash %x (expected %x)", gotSHA256Sum, m.WantSHA256Sum[:])
|
||
}
|
||
|
||
// Success! But this doesn't result in any real authentication, only a
|
||
// lack of authentication errors, so we return a nil result.
|
||
return nil, nil
|
||
}
|
||
|
||
type signatureAuthentication struct {
|
||
Document []byte
|
||
Signature []byte
|
||
Keys []SigningKey
|
||
ProviderSource addrs.Provider
|
||
Meta PackageMeta
|
||
}
|
||
|
||
// NewSignatureAuthentication returns a PackageAuthentication implementation
|
||
// that verifies the cryptographic signature for a package against any of the
|
||
// provided keys.
|
||
//
|
||
// The signing key for a package will be auto detected by attempting each key
|
||
// in turn until one is successful. If such a key is found, there are three
|
||
// possible successful authentication results:
|
||
//
|
||
// 1. If the signing key is the HashiCorp official key, it is an official
|
||
// provider;
|
||
// 2. Otherwise, if the signing key has a trust signature from the HashiCorp
|
||
// Partners key, it is a partner provider;
|
||
// 3. If neither of the above is true, it is a community provider.
|
||
//
|
||
// Any failure in the process of validating the signature will result in an
|
||
// unauthenticated result.
|
||
func NewSignatureAuthentication(meta PackageMeta, document, signature []byte, keys []SigningKey, source addrs.Provider) PackageAuthentication {
|
||
return signatureAuthentication{
|
||
Document: document,
|
||
Signature: signature,
|
||
Keys: keys,
|
||
ProviderSource: source,
|
||
Meta: meta,
|
||
}
|
||
}
|
||
|
||
// ErrUnknownIssuer indicates an error when no valid signature for a provider could be found.
|
||
var ErrUnknownIssuer = fmt.Errorf("authentication signature from unknown issuer")
|
||
|
||
// ShouldEnforceGPGValidationForProvider returns true if GPG signature
|
||
// validation must be enforced for the given provider.
|
||
//
|
||
// OpenTofu requires a valid GPG signature for any provider for which this
|
||
// function returns true. The result of this function only applies if the
|
||
// provider's origin registry does not return any signing keys for the
|
||
// provider; GPG signature is always required for any provider whose
|
||
// origin registry returns a signing key.
|
||
//
|
||
// The situations where this function returns false are part of a pragmatic
|
||
// compromise to allow the main OpenTofu registry to serve providers for
|
||
// which it does not currently know a signing key. For more information,
|
||
// refer to:
|
||
//
|
||
// https://github.com/opentofu/opentofu/issues/266
|
||
//
|
||
// The result of this function also determines whether signature
|
||
// verification is required in order for a particular hash to be tracked
|
||
// for the given provider in a dependency lock file. If this returns
|
||
// false then the dependency lock file should include any hash that
|
||
// was reported by the provider's origin registry, even if not signed.
|
||
func ShouldEnforceGPGValidationForProvider(addr addrs.Provider) bool {
|
||
// GPG verification is always required for everything except the main
|
||
// OpenTofu registry, since our possibility of skipping verification
|
||
// is a concession to allow our official registry to distribute
|
||
// providers that we don't have known private keys for, in which
|
||
// case we're relying on the TLS certificate authentication of the
|
||
// registry server as an alternative mechanism. (The registry
|
||
// is ultimately what reports which keys would be valid anyway, so
|
||
// if someone is able to compromise the connection to the registry
|
||
// then they could arrange for it to report any signing key they like.)
|
||
if addr.Hostname != addrs.DefaultProviderRegistryHost {
|
||
return true
|
||
}
|
||
|
||
// For the primary registry we allow providers that don't have
|
||
// GPG keys by default, but allow operators to opt out of this
|
||
// special exception using an environment variable.
|
||
enforceEnvVar, exists := os.LookupEnv(enforceGPGValidationEnvName)
|
||
return exists && enforceEnvVar == "true"
|
||
}
|
||
|
||
func (s signatureAuthentication) shouldEnforceGPGValidation() bool {
|
||
// If the registry returned at least one signing key then validation is always required.
|
||
if len(s.Keys) > 0 {
|
||
return true
|
||
}
|
||
|
||
// Otherwise the policy varies depending on what provider is being authenticated.
|
||
return ShouldEnforceGPGValidationForProvider(s.ProviderSource)
|
||
}
|
||
|
||
func (s signatureAuthentication) shouldEnforceGPGExpiration() bool {
|
||
// otherwise if the environment variable is set to true, we should enforce GPG expiration
|
||
enforceEnvVar, exists := os.LookupEnv(enforceGPGExpirationEnvName)
|
||
return exists && enforceEnvVar == "true"
|
||
}
|
||
|
||
func (s signatureAuthentication) AuthenticatePackage(location PackageLocation) (*PackageAuthenticationResult, error) {
|
||
shouldValidate := s.shouldEnforceGPGValidation()
|
||
|
||
var signingKeyIDs collections.Set[string]
|
||
if shouldValidate {
|
||
log.Printf("[DEBUG] Validating GPG signature of provider package %s", location)
|
||
|
||
_, keyID, err := s.findSigningKey()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("the provider is not signed with a valid signing key; please contact the provider author (%w)", err)
|
||
}
|
||
signingKeyIDs = collections.NewSet(keyID)
|
||
} else {
|
||
// As this is a temporary measure, we will log a warning to the user making it very clear what is happening
|
||
// and why. This will be removed in a future release.
|
||
log.Printf("[WARN] Skipping GPG validation of provider package %s as no keys were provided by the registry. See https://github.com/opentofu/opentofu/pull/309 for more information.", location)
|
||
}
|
||
|
||
// For each of the hashes mentioned in the document that was signed we'll announce that
|
||
// it was reported by the provider's origin registry, since that's the only place that
|
||
// this kind of signed hash file can come from, and _possibly_ report the key IDs that
|
||
// signed it unless we decided above that validation wasn't actually needed.
|
||
hashes := make(HashDispositions)
|
||
for _, hash := range s.acceptableHashes() {
|
||
hashes[hash] = &HashDisposition{
|
||
ReportedByRegistry: true,
|
||
SignedByGPGKeyIDs: signingKeyIDs,
|
||
}
|
||
}
|
||
return &PackageAuthenticationResult{hashes: hashes}, nil
|
||
}
|
||
|
||
func (s signatureAuthentication) acceptableHashes() []Hash {
|
||
// This is a bit of an abstraction leak because signatureAuthentication
|
||
// otherwise just treats the document as an opaque blob that's been
|
||
// signed, but here we're making assumptions about its format because
|
||
// we only want to trust that _all_ of the checksums are valid (rather
|
||
// than just the current platform's one) if we've also verified that the
|
||
// bag of checksums is signed.
|
||
//
|
||
// In recognition of that layering quirk this implementation is intended to
|
||
// be somewhat resilient to potentially using this authenticator with
|
||
// non-checksums files in future (in which case it'll return nothing at all)
|
||
// but it might be better in the long run to instead combine
|
||
// signatureAuthentication and matchingChecksumAuthentication together and
|
||
// be explicit that the resulting merged authenticator is exclusively for
|
||
// checksums files.
|
||
|
||
var ret []Hash
|
||
sc := bufio.NewScanner(bytes.NewReader(s.Document))
|
||
for sc.Scan() {
|
||
parts := bytes.Fields(sc.Bytes())
|
||
if len(parts) != 0 && len(parts) < 2 {
|
||
// Doesn't look like a valid sums file line, so we'll assume
|
||
// this whole thing isn't a checksums file.
|
||
return nil
|
||
}
|
||
|
||
// If this is a checksums file then the first part should be a
|
||
// hex-encoded SHA256 hash, so it should be 64 characters long
|
||
// and contain only hex digits.
|
||
hashStr := parts[0]
|
||
if len(hashStr) != 64 {
|
||
return nil // doesn't look like a checksums file
|
||
}
|
||
|
||
var gotSHA256Sum [sha256.Size]byte
|
||
if _, err := hex.Decode(gotSHA256Sum[:], hashStr); err != nil {
|
||
return nil // doesn't look like a checksums file
|
||
}
|
||
|
||
ret = append(ret, HashLegacyZipSHAFromSHA(gotSHA256Sum))
|
||
}
|
||
|
||
return ret
|
||
}
|
||
|
||
// findSigningKey attempts to verify the signature using each of the keys
|
||
// returned by the registry. If a valid signature is found, it returns the
|
||
// signing key.
|
||
//
|
||
// Note: currently the registry only returns one key, but this may change in
|
||
// the future.
|
||
func (s signatureAuthentication) findSigningKey() (*SigningKey, string, error) {
|
||
var expiredKey *SigningKey
|
||
var expiredKeyID string
|
||
|
||
for _, key := range s.Keys {
|
||
keyCopy := key
|
||
keyring, err := openpgp.ReadArmoredKeyRing(strings.NewReader(key.ASCIIArmor))
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("error decoding signing key: %w", err)
|
||
}
|
||
|
||
entity, err := openpgp.CheckDetachedSignature(keyring, bytes.NewReader(s.Document), bytes.NewReader(s.Signature), nil)
|
||
if errors.Is(err, openpgpErrors.ErrUnknownIssuer) {
|
||
continue
|
||
}
|
||
|
||
if err != nil {
|
||
// If in enforcing mode (or if the error isn’t related to expiry) return immediately.
|
||
if !errors.Is(err, openpgpErrors.ErrKeyExpired) && !errors.Is(err, openpgpErrors.ErrSignatureExpired) {
|
||
return nil, "", fmt.Errorf("error checking signature: %w", err)
|
||
}
|
||
|
||
// Else if it's an expired key then save it for later incase we don't find a non‐expired key.
|
||
if expiredKey == nil {
|
||
expiredKey = &keyCopy
|
||
if entity != nil && entity.PrimaryKey != nil {
|
||
expiredKeyID = entity.PrimaryKey.KeyIdString()
|
||
} else {
|
||
expiredKeyID = "n/a"
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
// Success! This key verified without an error.
|
||
keyID := "n/a"
|
||
if entity.PrimaryKey != nil {
|
||
keyID = entity.PrimaryKey.KeyIdString()
|
||
}
|
||
log.Printf("[DEBUG] Provider signed by %s", entityString(entity))
|
||
return &key, keyID, nil
|
||
}
|
||
|
||
// Warn only once when ALL keys are expired.
|
||
if expiredKey != nil && !s.shouldEnforceGPGExpiration() {
|
||
fmt.Printf("[WARN] Provider %s/%s (%v) gpg key expired, this will fail in future versions of OpenTofu\n",
|
||
s.Meta.Provider.Namespace, s.Meta.Provider.Type, s.Meta.Provider.Hostname)
|
||
return expiredKey, expiredKeyID, nil
|
||
}
|
||
|
||
// If we got here, no candidate was acceptable.
|
||
return nil, "", ErrUnknownIssuer
|
||
}
|
||
|
||
// entityString extracts the key ID and identity name(s) from an openpgp.Entity
|
||
// for logging.
|
||
func entityString(entity *openpgp.Entity) string {
|
||
if entity == nil {
|
||
return ""
|
||
}
|
||
|
||
keyID := "n/a"
|
||
if entity.PrimaryKey != nil {
|
||
keyID = entity.PrimaryKey.KeyIdString()
|
||
}
|
||
|
||
var names []string
|
||
for _, identity := range entity.Identities {
|
||
names = append(names, identity.Name)
|
||
}
|
||
|
||
return fmt.Sprintf("%s %s", keyID, strings.Join(names, ", "))
|
||
}
|