Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/golangci-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ jobs:
with:
fetch-depth: 0
- name: Install Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v7
uses: golangci/golangci-lint-action@v9
with:
version: v2.4
version: v2.12.2
args: -v

10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
module carvel.dev/vendir

go 1.25.7
go 1.26.5

require (
carvel.dev/imgpkg v0.48.0
carvel.dev/imgpkg v0.48.1
github.com/bmatcuk/doublestar v1.2.1
github.com/carvel-dev/semver/v4 v4.0.1
github.com/cppforlife/cobrautil v0.0.0-20221021151949-d60711905d65
Expand All @@ -18,7 +18,7 @@ require (
github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5
github.com/spf13/cobra v1.10.2
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.51.0
golang.org/x/crypto v0.52.0
golang.org/x/oauth2 v0.30.0
golang.org/x/tools v0.44.0
gopkg.in/inf.v0 v0.9.1
Expand Down Expand Up @@ -92,10 +92,10 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/sirupsen/logrus v1.9.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/vbatts/tar-split v0.12.1 // indirect
github.com/vito/go-interact v1.0.1 // indirect
golang.org/x/mod v0.35.0 // indirect
golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
Expand Down
15 changes: 8 additions & 7 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
carvel.dev/imgpkg v0.48.0 h1:rGcP3Rcf3w7N6wypXvpKcEca5ExQaZx+QywxXDAd1K8=
carvel.dev/imgpkg v0.48.0/go.mod h1:giklygGY0Z/r4ymSTMmXff/g1GI93WkwOMqpykC31A0=
carvel.dev/imgpkg v0.48.1 h1:Ea6UufZKqSg9S6Bf1s83ye9GoLTnnAudIEDLSww3C7w=
carvel.dev/imgpkg v0.48.1/go.mod h1:5+V0WDTaVOxnUosJ/6btoL7rXvmDCWYlBYV3BS9LBhg=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU=
cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo=
Expand Down Expand Up @@ -304,8 +304,9 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
Expand All @@ -330,8 +331,8 @@ golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190312203227-4b39c73a6495 h1:I6A9Ag9FpEKOjcKrRNjQkPHawoXIhKyTGfvvjFAiiAk=
Expand All @@ -344,8 +345,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down
5 changes: 5 additions & 0 deletions pkg/vendir/config/directory.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ type DirectoryContentsGit struct {

type DirectoryContentsGitVerification struct {
PublicKeysSecretRef *DirectoryContentsLocalRef `json:"publicKeysSecretRef,omitempty"`
// AllowLegacySignatures, under GODEBUG=fips140=only, downgrades a
// non-FIPS-approved signature algorithm (SHA-1, MD5, DSA) from an
// error to a warning instead of rejecting it.
// +optional
AllowLegacySignatures bool `json:"allowLegacySignatures,omitempty"`
}

type DirectoryContentsHg struct {
Expand Down
5 changes: 4 additions & 1 deletion pkg/vendir/fetch/git/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,10 @@ func (t *Git) fetch(dstPath string, tempArea ctlfetch.TempArea, bundle string) e
}

if t.opts.Verification != nil {
err := Verification{dstPath, *t.opts.Verification, t.refFetcher}.Verify(ref)
verification := Verification{
dstPath, *t.opts.Verification, t.refFetcher, t.infoLog,
}
err := verification.Verify(ref)
if err != nil {
return err
}
Expand Down
104 changes: 102 additions & 2 deletions pkg/vendir/fetch/git/verification.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@ package git

import (
"bytes"
"crypto"
"crypto/fips140"
"fmt"
"io"
"os/exec"
"strings"

ctlconf "carvel.dev/vendir/pkg/vendir/config"
ctlfetch "carvel.dev/vendir/pkg/vendir/fetch"
oarmor "carvel.dev/vendir/pkg/vendir/openpgparmor"
"golang.org/x/crypto/openpgp" //nolint:staticcheck
"golang.org/x/crypto/openpgp" //nolint:staticcheck
Comment thread
sameerforge marked this conversation as resolved.
"golang.org/x/crypto/openpgp/armor" //nolint:staticcheck
"golang.org/x/crypto/openpgp/packet" //nolint:staticcheck
)

// Verification verifies Git commit/tag against a set of public keys
Expand All @@ -21,6 +26,7 @@ type Verification struct {
repoPath string
opts ctlconf.DirectoryContentsGitVerification
refFetcher ctlfetch.RefFetcher
infoLog io.Writer
}

func (v Verification) Verify(ref string) error {
Expand Down Expand Up @@ -49,10 +55,27 @@ func (v Verification) Verify(ref string) error {
return err
}

// Non-FIPS-approved algorithms (SHA-1, MD5, DSA) panic under
// GODEBUG=fips140=only; reject or warn before that happens.
if fips140.Enforced() {
err := v.checkFIPSApproved(ref, signedObj.Signature)
if err != nil {
return err
}
}

target := strings.NewReader(signedObj.Contents)
sig := strings.NewReader(signedObj.Signature)

_, err = openpgp.CheckArmoredDetachedSignature(publicKeys, target, sig)
verify := func() {
_, err = openpgp.CheckArmoredDetachedSignature(publicKeys, target, sig)
}
if v.opts.AllowLegacySignatures {
// May be a non-approved algorithm let through above.
fips140.WithoutEnforcement(verify)
} else {
verify()
}
if err != nil {
hintMsg := ""
if strings.Contains(err.Error(), "signature made by unknown entity") {
Expand All @@ -64,6 +87,83 @@ func (v Verification) Verify(ref string) error {
return nil
}

// checkFIPSApproved errors on a non-FIPS-approved sig, or just warns
// if v.opts.AllowLegacySignatures is set.
func (v Verification) checkFIPSApproved(ref, sig string) error {
hashFunc, pubKeyAlgo, err := signatureAlgorithms(strings.NewReader(sig))
if err != nil {
return fmt.Errorf("Reading signature: %s", err)
}

var nonApprovedReasons []string
if !fipsApprovedHash(hashFunc) {
nonApprovedReasons = append(nonApprovedReasons,
fmt.Sprintf("hash algorithm %s", hashFunc))
}
if !fipsApprovedPubKeyAlgo(pubKeyAlgo) {
nonApprovedReasons = append(nonApprovedReasons,
fmt.Sprintf("pubkey algorithm %v", pubKeyAlgo))
}
if len(nonApprovedReasons) == 0 {
return nil
}
list := strings.Join(nonApprovedReasons, ", ")

if !v.opts.AllowLegacySignatures {
return fmt.Errorf("Checking signature for '%s': uses"+
" non-FIPS-approved %s; set verification.allowLegacySignatures"+
" to sync repositories with legacy-signed commits/tags under"+
" GODEBUG=fips140=only", ref, list)
}

fmt.Fprintf(v.infoLog, "Warning: signature for '%s' uses"+
" non-FIPS-approved %s\n", ref, list)

return nil
}

// signatureAlgorithms returns an armored detached signature's hash and
// public-key algorithms, without verifying it.
func signatureAlgorithms(r io.Reader) (
crypto.Hash, packet.PublicKeyAlgorithm, error,
) {
block, err := armor.Decode(r)
if err != nil {
return 0, 0, err
}

p, err := packet.Read(block.Body)
if err != nil {
return 0, 0, err
}

switch sig := p.(type) {
case *packet.Signature:
return sig.Hash, sig.PubKeyAlgo, nil
case *packet.SignatureV3:
return sig.Hash, sig.PubKeyAlgo, nil
default:
return 0, 0, fmt.Errorf("expected a signature packet, got %T", p)
}
}

// fipsApprovedHash reports whether hash is FIPS 140-3 approved.
// SHA-1 and MD5 are not, and panic under GODEBUG=fips140=only.
func fipsApprovedHash(hash crypto.Hash) bool {
switch hash {
case crypto.SHA1, crypto.MD5, crypto.MD5SHA1:
return false
default:
return true
}
}

// fipsApprovedPubKeyAlgo reports whether algo is FIPS 140-3 approved.
// DSA is not, and panics under GODEBUG=fips140=only.
func fipsApprovedPubKeyAlgo(algo packet.PublicKeyAlgorithm) bool {
return algo != packet.PubKeyAlgoDSA
}

type signedObj struct {
Contents string
Signature string
Expand Down
109 changes: 109 additions & 0 deletions pkg/vendir/fetch/git/verification_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2026 The Carvel Authors.
// SPDX-License-Identifier: Apache-2.0

package git

import (
"bytes"
"crypto"
"strings"
"testing"

ctlconf "carvel.dev/vendir/pkg/vendir/config"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/openpgp/packet" //nolint:staticcheck
)

// Fixed armored detached signatures over the literal text "hello world\n",
// pre-generated with a throwaway GPG key so these tests don't need gpg on
// PATH. approvedSig is RSA+SHA256 (FIPS-approved); legacyHashSig is the
// same RSA key forced to sign with SHA1 (non-approved hash); legacyDSASig
// is a 1024-bit DSA key (non-approved pubkey algorithm).
const (
approvedSig = `-----BEGIN PGP SIGNATURE-----

iQEzBAABCAAdFiEEQLsnJrIjJBKz+S0Go2HoPiOrblcFAmqH8IQACgkQo2HoPiOr
bleXvQgAgZpJZpuaL3ku/7Oxm/OXF7g0cLwCgbpVzqieSDQ7b/SNWo/NWhPt76V8
YH9fDXgt6yxT4ukqOvNW4ESnzLU9hRNy7Me1h83L/njhLyOqWoUaewBhcuTWTE13
uCF2zSi2O/oCnZ8ZjwHnf6rIR9082mpI4o2zJVTmsw+L0p56fm2vM2QPMjNLNXzC
5CjSgwLC8j+ilklW21wGJVeM+IIC35b8Dl/SqTzpvLKnREwxNdJz8CZnuYzS9KFa
5OSaTmLFq6dy2DVRRBVPe+dfEj8E07MCNvPCAXOVgLnmqpq4gvpZ06UaHt6FfD9u
P/jc8MSTszFZiBk2ipIuuSN4NilYaA==
=sZsk
-----END PGP SIGNATURE-----
`
legacyHashSig = `-----BEGIN PGP SIGNATURE-----

iQEzBAABAgAdFiEEQLsnJrIjJBKz+S0Go2HoPiOrblcFAmqH8IQACgkQo2HoPiOr
blcLdgf+OSp4P/JC9jG4x8BolhaEJOxGC0pFPbjoRex9zMzyspQzVos+oF2ARq3M
J0SWRSOd1IrDYvgCEstUJvlpWr97P6zJW830NQgr+0Wp7MN3SOkL1sjp1Ax79GmF
DyJ6UFGAslwSBWS9LnpgfOF1lhClExKf2KMMrTwpDpP2qEl/rGV3LP7nRNXBlP0s
6AJdtEoJrUsJqYavqVT8i78juiWSD8JVFtspz15dyZ9dDA9JhTS1QjgBOGJ5sEhj
fTsbePrtmk6TgjEFrs6xt2L4LVZHt8ypyy/H3OpBOY6+/eqr4Yy60DZhV3JRZrHE
qXOThcpTEGCWYakFGjHn0mGpPPXekg==
=S6/W
-----END PGP SIGNATURE-----
`
legacyDSASig = `-----BEGIN PGP SIGNATURE-----

iF0EABECAB0WIQSNpoTJgEs/HmLRkJ5eLmKg5upf1wUCaofwhAAKCRBeLmKg5upf
10mxAKCoI6PhKxq7bNM9q/6e5kIcN/+RggCaAgMZE9LTYajEEvt4oPjfFxKbVec=
=oCds
-----END PGP SIGNATURE-----
`
)

func TestSignatureAlgorithms(t *testing.T) {
hash, algo, err := signatureAlgorithms(strings.NewReader(approvedSig))
require.NoError(t, err)
require.Equal(t, crypto.SHA256, hash)
require.Equal(t, packet.PubKeyAlgoRSA, algo)

hash, algo, err = signatureAlgorithms(strings.NewReader(legacyHashSig))
require.NoError(t, err)
require.Equal(t, crypto.SHA1, hash)
require.Equal(t, packet.PubKeyAlgoRSA, algo)

hash, algo, err = signatureAlgorithms(strings.NewReader(legacyDSASig))
require.NoError(t, err)
require.Equal(t, crypto.SHA1, hash)
require.Equal(t, packet.PubKeyAlgoDSA, algo)
}

func TestCheckFIPSApproved(t *testing.T) {
t.Run("approved signature never errors or warns", func(t *testing.T) {
var buf bytes.Buffer
v := Verification{infoLog: &buf}
require.NoError(t, v.checkFIPSApproved("ref", approvedSig))
require.Empty(t, buf.String())
})

t.Run("legacy hash errors by default", func(t *testing.T) {
v := Verification{infoLog: &bytes.Buffer{}}
err := v.checkFIPSApproved("myref", legacyHashSig)
require.ErrorContains(t, err, "myref")
require.ErrorContains(t, err, "hash algorithm SHA-1")
require.ErrorContains(t, err, "verification.allowLegacySignatures")
})

t.Run("legacy DSA errors by default", func(t *testing.T) {
v := Verification{infoLog: &bytes.Buffer{}}
err := v.checkFIPSApproved("myref", legacyDSASig)
require.ErrorContains(t, err, "pubkey algorithm")
require.ErrorContains(t, err, "verification.allowLegacySignatures")
})

t.Run("legacy signatures only warn when allowed", func(t *testing.T) {
var buf bytes.Buffer
opts := ctlconf.DirectoryContentsGitVerification{AllowLegacySignatures: true}
v := Verification{infoLog: &buf, opts: opts}

require.NoError(t, v.checkFIPSApproved("myref", legacyHashSig))
require.Contains(t, buf.String(), "myref")
require.Contains(t, buf.String(), "hash algorithm SHA-1")

buf.Reset()
require.NoError(t, v.checkFIPSApproved("myref", legacyDSASig))
require.Contains(t, buf.String(), "pubkey algorithm")
})
}
13 changes: 12 additions & 1 deletion pkg/vendir/openpgparmor/armor.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package openpgparmor

import (
"crypto/fips140"
"fmt"
"strings"

Expand All @@ -25,7 +26,17 @@ func ReadArmoredKeys(keys string) (openpgp.EntityList, error) {
continue
}

el, err := openpgp.ReadArmoredKeyRing(strings.NewReader(startMarker + part))
// Parsing an OpenPGP key packet unconditionally computes its
// RFC 4880 V4 fingerprint using SHA-1, which panics under
// GODEBUG=fips140=only. That fingerprint is used only as a
// key identifier for later signature lookups (see
// fetch/git/verification.go).
var el openpgp.EntityList
var err error
fips140.WithoutEnforcement(func() {
r := strings.NewReader(startMarker + part)
el, err = openpgp.ReadArmoredKeyRing(r)
})
if err != nil {
return nil, fmt.Errorf("Reading armored key [idx=%d]: %s", i, err)
}
Expand Down
Loading
Loading