policy: add HTTP PGP signature verification builtin
Add verify_http_pgp_signature Rego builtin for HTTP sources using pgpsign with checksum-request/response flow through policy resolution. Wire sourcemeta HTTP checksum request/response conversion and add tests. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
@@ -5,6 +5,7 @@ go 1.25.5
|
|||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver/v3 v3.4.0
|
github.com/Masterminds/semver/v3 v3.4.0
|
||||||
github.com/Microsoft/go-winio v0.6.2
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
|
github.com/ProtonMail/go-crypto v1.3.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.32.12
|
github.com/aws/aws-sdk-go-v2/config v1.32.12
|
||||||
github.com/compose-spec/compose-go/v2 v2.9.1
|
github.com/compose-spec/compose-go/v2 v2.9.1
|
||||||
github.com/containerd/console v1.0.5
|
github.com/containerd/console v1.0.5
|
||||||
@@ -78,7 +79,6 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
|
||||||
github.com/agext/levenshtein v1.2.3 // indirect
|
github.com/agext/levenshtein v1.2.3 // indirect
|
||||||
github.com/agnivade/levenshtein v1.2.1 // indirect
|
github.com/agnivade/levenshtein v1.2.1 // indirect
|
||||||
github.com/apparentlymart/go-cidr v1.0.1 // indirect
|
github.com/apparentlymart/go-cidr v1.0.1 // indirect
|
||||||
|
|||||||
+138
-5
@@ -3,11 +3,13 @@ package policy
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"maps"
|
"maps"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
@@ -15,8 +17,10 @@ import (
|
|||||||
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||||
"github.com/moby/buildkit/client/llb"
|
"github.com/moby/buildkit/client/llb"
|
||||||
gwclient "github.com/moby/buildkit/frontend/gateway/client"
|
gwclient "github.com/moby/buildkit/frontend/gateway/client"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
"github.com/moby/buildkit/solver/pb"
|
"github.com/moby/buildkit/solver/pb"
|
||||||
"github.com/moby/buildkit/util/gitutil/gitsign"
|
"github.com/moby/buildkit/util/gitutil/gitsign"
|
||||||
|
"github.com/moby/buildkit/util/pgpsign"
|
||||||
"github.com/open-policy-agent/opa/v1/ast"
|
"github.com/open-policy-agent/opa/v1/ast"
|
||||||
"github.com/open-policy-agent/opa/v1/rego"
|
"github.com/open-policy-agent/opa/v1/rego"
|
||||||
"github.com/open-policy-agent/opa/v1/types"
|
"github.com/open-policy-agent/opa/v1/types"
|
||||||
@@ -26,11 +30,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
funcLoadJSON = "load_json"
|
funcLoadJSON = "load_json"
|
||||||
funcVerifyGitSignature = "verify_git_signature"
|
funcVerifyGitSignature = "verify_git_signature"
|
||||||
funcPinImage = "pin_image"
|
funcVerifyHTTPPGPSignature = "verify_http_pgp_signature"
|
||||||
funcArtifactAttestation = "artifact_attestation"
|
funcPinImage = "pin_image"
|
||||||
funcGithubAttestation = "github_attestation"
|
funcArtifactAttestation = "artifact_attestation"
|
||||||
|
funcGithubAttestation = "github_attestation"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (p *Policy) initBuiltinFuncs() {
|
func (p *Policy) initBuiltinFuncs() {
|
||||||
@@ -69,6 +74,27 @@ func (p *Policy) initBuiltinFuncs() {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
verifyHTTPPGPSignature := ®o.Function{
|
||||||
|
Name: funcVerifyHTTPPGPSignature,
|
||||||
|
Decl: types.NewFunction(
|
||||||
|
types.Args(
|
||||||
|
types.A,
|
||||||
|
types.S,
|
||||||
|
types.S,
|
||||||
|
),
|
||||||
|
types.B,
|
||||||
|
),
|
||||||
|
Memoize: false, // TODO:optimize
|
||||||
|
}
|
||||||
|
p.funcs = append(p.funcs, fun{
|
||||||
|
decl: verifyHTTPPGPSignature,
|
||||||
|
impl: func(s *state) func(*rego.Rego) {
|
||||||
|
return rego.Function3(verifyHTTPPGPSignature, func(bctx rego.BuiltinContext, a1 *ast.Term, a2 *ast.Term, a3 *ast.Term) (*ast.Term, error) {
|
||||||
|
return p.builtinVerifyHTTPPGPSignatureImpl(bctx, a1, a2, a3, s)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
pinImageDigest := ®o.Function{
|
pinImageDigest := ®o.Function{
|
||||||
Name: funcPinImage,
|
Name: funcPinImage,
|
||||||
Decl: types.NewFunction(
|
Decl: types.NewFunction(
|
||||||
@@ -482,6 +508,113 @@ func (p *Policy) builtinVerifyGitSignatureImpl(_ rego.BuiltinContext, a1, a2 *as
|
|||||||
return ast.BooleanTerm(true), nil
|
return ast.BooleanTerm(true), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Policy) builtinVerifyHTTPPGPSignatureImpl(_ rego.BuiltinContext, a1, a2, a3 *ast.Term, s *state) (*ast.Term, error) {
|
||||||
|
inp := s.Input
|
||||||
|
if inp.HTTP == nil {
|
||||||
|
return ast.BooleanTerm(false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
obja, ok := a1.Value.(ast.Object)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("%s: expected object, got %T", funcVerifyHTTPPGPSignature, a1.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpValue, err := ast.InterfaceToValue(inp.HTTP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%s: failed converting object to interface", funcVerifyHTTPPGPSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
if obja.Compare(httpValue) != 0 {
|
||||||
|
return nil, errors.Errorf("%s: first argument is not the same as input http", funcVerifyHTTPPGPSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigPath, ok := a2.Value.(ast.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("%s: expected string signature path, got %T", funcVerifyHTTPPGPSignature, a2.Value)
|
||||||
|
}
|
||||||
|
pubKeyPath, ok := a3.Value.(ast.String)
|
||||||
|
if !ok {
|
||||||
|
return nil, errors.Errorf("%s: expected string pubkey path, got %T", funcVerifyHTTPPGPSignature, a3.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
signatureData, err := p.readFile(string(sigPath), 512*1024)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pubKeyData, err := p.readFile(string(pubKeyPath), 512*1024)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sig, _, err := pgpsign.ParseArmoredDetachedSignature(signatureData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%s: failed to parse detached signature", funcVerifyHTTPPGPSignature)
|
||||||
|
}
|
||||||
|
keyring, err := pgpsign.ReadAllArmoredKeyRings(pubKeyData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%s: failed to read armored keyring", funcVerifyHTTPPGPSignature)
|
||||||
|
}
|
||||||
|
|
||||||
|
algo, err := toPBChecksumAlgo(sig.Hash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%s: unsupported signature hash", funcVerifyHTTPPGPSignature)
|
||||||
|
}
|
||||||
|
suffix := slices.Clone(sig.HashSuffix)
|
||||||
|
checksumReq := &gwpb.ChecksumRequest{Algo: algo, Suffix: suffix}
|
||||||
|
|
||||||
|
resp := inp.HTTP.checksumResponseForSignature
|
||||||
|
if resp == nil || resp.Digest == "" {
|
||||||
|
s.checksumNeededForSignature = checksumReq
|
||||||
|
s.addUnknown(funcVerifyHTTPPGPSignature)
|
||||||
|
return ast.BooleanTerm(false), nil
|
||||||
|
}
|
||||||
|
if !bytes.Equal(resp.Suffix, suffix) {
|
||||||
|
s.checksumNeededForSignature = checksumReq
|
||||||
|
s.addUnknown(funcVerifyHTTPPGPSignature)
|
||||||
|
return ast.BooleanTerm(false), nil
|
||||||
|
}
|
||||||
|
dgst, err := digest.Parse(resp.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "%s: invalid checksum digest", funcVerifyHTTPPGPSignature)
|
||||||
|
}
|
||||||
|
if !checksumAlgoMatches(algo, dgst.Algorithm()) {
|
||||||
|
s.checksumNeededForSignature = checksumReq
|
||||||
|
s.addUnknown(funcVerifyHTTPPGPSignature)
|
||||||
|
return ast.BooleanTerm(false), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := pgpsign.VerifySignatureWithDigest(sig, keyring, dgst); err != nil {
|
||||||
|
return ast.BooleanTerm(false), nil
|
||||||
|
}
|
||||||
|
return ast.BooleanTerm(true), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPBChecksumAlgo(hash crypto.Hash) (gwpb.ChecksumRequest_ChecksumAlgo, error) {
|
||||||
|
switch hash {
|
||||||
|
case crypto.SHA256:
|
||||||
|
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA256, nil
|
||||||
|
case crypto.SHA384:
|
||||||
|
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA384, nil
|
||||||
|
case crypto.SHA512:
|
||||||
|
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA512, nil
|
||||||
|
default:
|
||||||
|
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA256, errors.Errorf("unsupported signature hash algorithm %v", hash)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checksumAlgoMatches(algo gwpb.ChecksumRequest_ChecksumAlgo, digestAlgo digest.Algorithm) bool {
|
||||||
|
switch algo {
|
||||||
|
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA256:
|
||||||
|
return digestAlgo == digest.SHA256
|
||||||
|
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA384:
|
||||||
|
return digestAlgo == digest.SHA384
|
||||||
|
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA512:
|
||||||
|
return digestAlgo == digest.SHA512
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Policy) readFile(path string, limit int64) ([]byte, error) {
|
func (p *Policy) readFile(path string, limit int64) ([]byte, error) {
|
||||||
if p.opt.FS == nil {
|
if p.opt.FS == nil {
|
||||||
return nil, errors.Errorf("no policy FS defined for reading context files")
|
return nil, errors.Errorf("no policy FS defined for reading context files")
|
||||||
|
|||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package policy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"encoding/hex"
|
||||||
|
"io/fs"
|
||||||
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||||
|
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||||
|
"github.com/moby/buildkit/util/pgpsign"
|
||||||
|
"github.com/open-policy-agent/opa/v1/ast"
|
||||||
|
"github.com/open-policy-agent/opa/v1/rego"
|
||||||
|
digest "github.com/opencontainers/go-digest"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuiltinVerifyHTTPPGPSignatureImpl(t *testing.T) {
|
||||||
|
const (
|
||||||
|
sigPath = "sig.asc"
|
||||||
|
keyPath = "pubkey.asc"
|
||||||
|
)
|
||||||
|
payload := []byte("buildx-http-payload")
|
||||||
|
sigData, pubKeyData, checksumDigest, suffix := createDetachedPGPFixture(t, payload)
|
||||||
|
|
||||||
|
newPolicy := func(sig []byte, pub []byte) *Policy {
|
||||||
|
return NewPolicy(Opt{
|
||||||
|
FS: func() (fs.StatFS, func() error, error) {
|
||||||
|
return fstest.MapFS{
|
||||||
|
sigPath: &fstest.MapFile{Data: sig},
|
||||||
|
keyPath: &fstest.MapFile{Data: pub},
|
||||||
|
}, func() error { return nil }, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("success", func(t *testing.T) {
|
||||||
|
st := &state{
|
||||||
|
Input: Input{
|
||||||
|
HTTP: &HTTP{
|
||||||
|
checksumResponseForSignature: &httpChecksumResponseForSignature{
|
||||||
|
Digest: checksumDigest.String(),
|
||||||
|
Suffix: append([]byte(nil), suffix...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p := newPolicy(sigData, pubKeyData)
|
||||||
|
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
|
||||||
|
rego.BuiltinContext{Context: t.Context()},
|
||||||
|
ast.NewTerm(httpVal),
|
||||||
|
ast.StringTerm(sigPath),
|
||||||
|
ast.StringTerm(keyPath),
|
||||||
|
st,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ast.BooleanTerm(true), got)
|
||||||
|
require.Nil(t, st.checksumNeededForSignature)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("verify-failure-returns-false", func(t *testing.T) {
|
||||||
|
encoded := checksumDigest.Encoded()
|
||||||
|
require.NotEmpty(t, encoded)
|
||||||
|
flipped := "0" + encoded[1:]
|
||||||
|
badDigest := digest.NewDigestFromEncoded(checksumDigest.Algorithm(), flipped)
|
||||||
|
|
||||||
|
st := &state{
|
||||||
|
Input: Input{
|
||||||
|
HTTP: &HTTP{
|
||||||
|
checksumResponseForSignature: &httpChecksumResponseForSignature{
|
||||||
|
Digest: badDigest.String(),
|
||||||
|
Suffix: append([]byte(nil), suffix...),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p := newPolicy(sigData, pubKeyData)
|
||||||
|
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
|
||||||
|
rego.BuiltinContext{Context: t.Context()},
|
||||||
|
ast.NewTerm(httpVal),
|
||||||
|
ast.StringTerm(sigPath),
|
||||||
|
ast.StringTerm(keyPath),
|
||||||
|
st,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ast.BooleanTerm(false), got)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing-checksum-response-returns-false-and-adds-unknown", func(t *testing.T) {
|
||||||
|
st := &state{
|
||||||
|
Input: Input{
|
||||||
|
HTTP: &HTTP{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p := newPolicy(sigData, pubKeyData)
|
||||||
|
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
|
||||||
|
rego.BuiltinContext{Context: t.Context()},
|
||||||
|
ast.NewTerm(httpVal),
|
||||||
|
ast.StringTerm(sigPath),
|
||||||
|
ast.StringTerm(keyPath),
|
||||||
|
st,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ast.BooleanTerm(false), got)
|
||||||
|
require.Contains(t, st.Unknowns, funcVerifyHTTPPGPSignature)
|
||||||
|
require.NotNil(t, st.checksumNeededForSignature)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid-signature-errors", func(t *testing.T) {
|
||||||
|
p := newPolicy([]byte("not-a-signature"), pubKeyData)
|
||||||
|
st := &state{Input: Input{HTTP: &HTTP{}}}
|
||||||
|
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
|
||||||
|
rego.BuiltinContext{Context: t.Context()},
|
||||||
|
ast.NewTerm(httpVal),
|
||||||
|
ast.StringTerm(sigPath),
|
||||||
|
ast.StringTerm(keyPath),
|
||||||
|
st,
|
||||||
|
)
|
||||||
|
require.Nil(t, got)
|
||||||
|
require.ErrorContains(t, err, "verify_http_pgp_signature: failed to parse detached signature")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDetachedPGPFixture(t *testing.T, payload []byte) ([]byte, []byte, digest.Digest, []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
entity, err := openpgp.NewEntity("buildx", "", "buildx@example.com", &packet.Config{
|
||||||
|
DefaultHash: crypto.SHA256,
|
||||||
|
RSABits: 2048,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var sigBuf bytes.Buffer
|
||||||
|
err = openpgp.ArmoredDetachSign(&sigBuf, entity, bytes.NewReader(payload), &packet.Config{
|
||||||
|
DefaultHash: crypto.SHA256,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
sigData := sigBuf.Bytes()
|
||||||
|
|
||||||
|
var pubBuf bytes.Buffer
|
||||||
|
aw, err := armor.Encode(&pubBuf, openpgp.PublicKeyType, nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = entity.Serialize(aw)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = aw.Close()
|
||||||
|
require.NoError(t, err)
|
||||||
|
pubKeyData := pubBuf.Bytes()
|
||||||
|
|
||||||
|
sig, _, err := pgpsign.ParseArmoredDetachedSignature(sigData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
h := sig.Hash.New()
|
||||||
|
_, err = h.Write(payload)
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = h.Write(sig.HashSuffix)
|
||||||
|
require.NoError(t, err)
|
||||||
|
sum := h.Sum(nil)
|
||||||
|
|
||||||
|
dAlgo := digest.SHA256
|
||||||
|
switch sig.Hash {
|
||||||
|
case crypto.SHA384:
|
||||||
|
dAlgo = digest.SHA384
|
||||||
|
case crypto.SHA512:
|
||||||
|
dAlgo = digest.SHA512
|
||||||
|
}
|
||||||
|
|
||||||
|
return sigData, pubKeyData, digest.NewDigestFromEncoded(dAlgo, hex.EncodeToString(sum)), append([]byte(nil), sig.HashSuffix...)
|
||||||
|
}
|
||||||
@@ -6,7 +6,9 @@ import (
|
|||||||
|
|
||||||
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
|
||||||
"github.com/moby/buildkit/client/llb/sourceresolver"
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
"github.com/moby/buildkit/solver/pb"
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/moby/buildkit/sourcepolicy/policysession"
|
||||||
"github.com/opencontainers/go-digest"
|
"github.com/opencontainers/go-digest"
|
||||||
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -66,3 +68,40 @@ func TestResolveInputUnknownsResolvesMaterialField(t *testing.T) {
|
|||||||
require.Nil(t, next)
|
require.Nil(t, next)
|
||||||
require.True(t, inp.Image.Provenance.Materials[0].Image.HasProvenance)
|
require.True(t, inp.Image.Provenance.Materials[0].Image.HasProvenance)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResolveInputUnknownsHTTPChecksumResponseRequest(t *testing.T) {
|
||||||
|
inp := Input{
|
||||||
|
HTTP: &HTTP{
|
||||||
|
URL: "https://example.com/file.tgz",
|
||||||
|
Schema: "https",
|
||||||
|
Host: "example.com",
|
||||||
|
Path: "/file.tgz",
|
||||||
|
Query: map[string][]string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
checksumReq := &gwpb.ChecksumRequest{
|
||||||
|
Algo: gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA384,
|
||||||
|
Suffix: []byte{0xaa, 0xbb, 0xcc},
|
||||||
|
}
|
||||||
|
|
||||||
|
p := NewPolicy(Opt{})
|
||||||
|
retry, next, err := p.resolveUnknowns(
|
||||||
|
context.Background(),
|
||||||
|
&inp,
|
||||||
|
&policysession.CheckPolicyRequest{
|
||||||
|
Source: &gwpb.ResolveSourceMetaResponse{
|
||||||
|
Source: &pb.SourceOp{Identifier: "https://example.com/file.tgz"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
&state{checksumNeededForSignature: checksumReq},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.False(t, retry)
|
||||||
|
require.NotNil(t, next)
|
||||||
|
require.NotNil(t, next.HTTP)
|
||||||
|
require.NotNil(t, next.HTTP.ChecksumRequest)
|
||||||
|
require.Equal(t, checksumReq.Algo, next.HTTP.ChecksumRequest.Algo)
|
||||||
|
require.Equal(t, checksumReq.Suffix, next.HTTP.ChecksumRequest.Suffix)
|
||||||
|
}
|
||||||
|
|||||||
+5
-3
@@ -41,11 +41,13 @@ type HTTP struct {
|
|||||||
|
|
||||||
Checksum string `json:"checksum,omitempty"`
|
Checksum string `json:"checksum,omitempty"`
|
||||||
|
|
||||||
Signature *PGPSignature `json:"signature,omitempty"`
|
checksumResponseForSignature *httpChecksumResponseForSignature `json:"-"`
|
||||||
AttestationBundle *AttestationBundle `json:"attestationBundle,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttestationBundle struct{}
|
type httpChecksumResponseForSignature struct {
|
||||||
|
Digest string `json:"-"`
|
||||||
|
Suffix []byte `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type Git struct {
|
type Git struct {
|
||||||
Schema string `json:"schema,omitempty"`
|
Schema string `json:"schema,omitempty"`
|
||||||
|
|||||||
@@ -111,9 +111,10 @@ func TestRuntimeUnknownInputRefs(t *testing.T) {
|
|||||||
|
|
||||||
st := &state{
|
st := &state{
|
||||||
Unknowns: map[string]struct{}{
|
Unknowns: map[string]struct{}{
|
||||||
funcVerifyGitSignature: {},
|
funcVerifyGitSignature: {},
|
||||||
funcArtifactAttestation: {},
|
funcArtifactAttestation: {},
|
||||||
funcGithubAttestation: {},
|
funcGithubAttestation: {},
|
||||||
|
funcVerifyHTTPPGPSignature: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
require.Equal(t, []string{"git.commit", "http.checksum"}, runtimeUnknownInputRefs(st))
|
require.Equal(t, []string{"git.commit", "http.checksum"}, runtimeUnknownInputRefs(st))
|
||||||
|
|||||||
+30
-4
@@ -45,7 +45,8 @@ type state struct {
|
|||||||
Input Input
|
Input Input
|
||||||
Unknowns map[string]struct{}
|
Unknowns map[string]struct{}
|
||||||
|
|
||||||
ImagePins map[digest.Digest]struct{}
|
ImagePins map[digest.Digest]struct{}
|
||||||
|
checksumNeededForSignature *gwpb.ChecksumRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *state) addUnknown(key string) {
|
func (s *state) addUnknown(key string) {
|
||||||
@@ -274,7 +275,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
unk := collectUnknowns(pq.Support, unknowns)
|
unk := collectUnknowns(pq.Support, unknowns)
|
||||||
unk = append(unk, runtimeUnknownInputRefs(st)...)
|
unk = append(unk, runtimeUnknownInputRefs(st)...)
|
||||||
|
|
||||||
retry, next, err := p.resolveUnknowns(ctx, &inp, req, platform, unk)
|
retry, next, err := p.resolveUnknowns(ctx, &inp, req, platform, unk, st)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -292,7 +293,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
retry, next, err := p.resolveUnknowns(ctx, &inp, req, platform, runtimeUnknownInputRefs(st))
|
retry, next, err := p.resolveUnknowns(ctx, &inp, req, platform, runtimeUnknownInputRefs(st), st)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -372,11 +373,29 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
|
|||||||
return nil, nil, errors.Errorf("maximum attempts reached for resolving policy metadata")
|
return nil, nil, errors.Errorf("maximum attempts reached for resolving policy metadata")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Policy) resolveUnknowns(ctx context.Context, input *Input, req *policysession.CheckPolicyRequest, defaultPlatform *ocispecs.Platform, unk []string) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
func (p *Policy) resolveUnknowns(ctx context.Context, input *Input, req *policysession.CheckPolicyRequest, defaultPlatform *ocispecs.Platform, unk []string, st *state) (bool, *gwpb.ResolveSourceMetaRequest, error) {
|
||||||
var resolver SourceMetadataResolver
|
var resolver SourceMetadataResolver
|
||||||
if p.opt.SourceResolver != nil {
|
if p.opt.SourceResolver != nil {
|
||||||
resolver = p.opt.SourceResolver
|
resolver = p.opt.SourceResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if st != nil && st.checksumNeededForSignature != nil {
|
||||||
|
next := &gwpb.ResolveSourceMetaRequest{
|
||||||
|
Source: req.Source.Source,
|
||||||
|
}
|
||||||
|
if req.Platform != nil {
|
||||||
|
next.Platform = req.Platform
|
||||||
|
}
|
||||||
|
if err := AddUnknownsWithLogger(p.opt.Log, next, normalizeNodeUnknowns(unk)); err != nil {
|
||||||
|
return false, nil, err
|
||||||
|
}
|
||||||
|
next.HTTP = &gwpb.ResolveSourceHTTPRequest{
|
||||||
|
ChecksumRequest: st.checksumNeededForSignature.CloneVT(),
|
||||||
|
}
|
||||||
|
p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", sourceName(req), summarizeUnknownsForLog(unk))
|
||||||
|
return false, next, nil
|
||||||
|
}
|
||||||
|
|
||||||
retry, next, err := ResolveInputUnknowns(ctx, input, req.Source.Source, unk, req.Platform, defaultPlatform, resolver, p.opt.VerifierProvider, p.opt.Log)
|
retry, next, err := ResolveInputUnknowns(ctx, input, req.Source.Source, unk, req.Platform, defaultPlatform, resolver, p.opt.VerifierProvider, p.opt.Log)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, nil, err
|
return false, nil, err
|
||||||
@@ -387,6 +406,7 @@ func (p *Policy) resolveUnknowns(ctx context.Context, input *Input, req *policys
|
|||||||
}
|
}
|
||||||
return retry, nil, nil
|
return retry, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func platformFromReq(req *policysession.CheckPolicyRequest) (*ocispecs.Platform, error) {
|
func platformFromReq(req *policysession.CheckPolicyRequest) (*ocispecs.Platform, error) {
|
||||||
if req.Platform != nil {
|
if req.Platform != nil {
|
||||||
platformStr := req.Platform.OS + "/" + req.Platform.Architecture
|
platformStr := req.Platform.OS + "/" + req.Platform.Architecture
|
||||||
@@ -446,6 +466,12 @@ func sourceToInput(ctx context.Context, getVerifier PolicyVerifierProvider, src
|
|||||||
}
|
}
|
||||||
if src.HTTP != nil {
|
if src.HTTP != nil {
|
||||||
inp.HTTP.Checksum = src.HTTP.Checksum
|
inp.HTTP.Checksum = src.HTTP.Checksum
|
||||||
|
if src.HTTP.ChecksumResponse != nil {
|
||||||
|
inp.HTTP.checksumResponseForSignature = &httpChecksumResponseForSignature{
|
||||||
|
Digest: src.HTTP.ChecksumResponse.Digest,
|
||||||
|
Suffix: slices.Clone(src.HTTP.ChecksumResponse.Suffix),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if inp.HTTP.Checksum == "" {
|
if inp.HTTP.Checksum == "" {
|
||||||
unknowns = append(unknowns, "input.http.checksum")
|
unknowns = append(unknowns, "input.http.checksum")
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ func ToResolverOpt(req *gwpb.ResolveSourceMetaRequest, defaultPlatform *ocispecs
|
|||||||
if req != nil && req.Git != nil {
|
if req != nil && req.Git != nil {
|
||||||
opt.GitOpt = &sourceresolver.ResolveGitOpt{ReturnObject: req.Git.ReturnObject}
|
opt.GitOpt = &sourceresolver.ResolveGitOpt{ReturnObject: req.Git.ReturnObject}
|
||||||
}
|
}
|
||||||
|
if req != nil && req.HTTP != nil && req.HTTP.ChecksumRequest != nil {
|
||||||
|
opt.HTTPOpt = &sourceresolver.ResolveHTTPOpt{
|
||||||
|
ChecksumReq: &sourceresolver.ResolveHTTPChecksumRequest{
|
||||||
|
Algo: toResolverChecksumAlgo(req.HTTP.ChecksumRequest.Algo),
|
||||||
|
Suffix: append([]byte(nil), req.HTTP.ChecksumRequest.Suffix...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
return opt
|
return opt
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +72,27 @@ func ToGatewayMetaResponse(resp *sourceresolver.MetaResponse) *gwpb.ResolveSourc
|
|||||||
Filename: resp.HTTP.Filename,
|
Filename: resp.HTTP.Filename,
|
||||||
LastModified: lastModified,
|
LastModified: lastModified,
|
||||||
}
|
}
|
||||||
|
if resp.HTTP.ChecksumResponse != nil {
|
||||||
|
out.HTTP.ChecksumResponse = &gwpb.ChecksumResponse{
|
||||||
|
Digest: resp.HTTP.ChecksumResponse.Digest,
|
||||||
|
Suffix: append([]byte(nil), resp.HTTP.ChecksumResponse.Suffix...),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toResolverChecksumAlgo(in gwpb.ChecksumRequest_ChecksumAlgo) sourceresolver.ResolveHTTPChecksumAlgo {
|
||||||
|
switch in {
|
||||||
|
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA384:
|
||||||
|
return sourceresolver.ResolveHTTPChecksumAlgoSHA384
|
||||||
|
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA512:
|
||||||
|
return sourceresolver.ResolveHTTPChecksumAlgoSHA512
|
||||||
|
default:
|
||||||
|
return sourceresolver.ResolveHTTPChecksumAlgoSHA256
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func toGatewayDescriptor(desc ocispecs.Descriptor) *gwpb.Descriptor {
|
func toGatewayDescriptor(desc ocispecs.Descriptor) *gwpb.Descriptor {
|
||||||
return &gwpb.Descriptor{
|
return &gwpb.Descriptor{
|
||||||
MediaType: desc.MediaType,
|
MediaType: desc.MediaType,
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package sourcemeta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/moby/buildkit/client/llb/sourceresolver"
|
||||||
|
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
|
||||||
|
"github.com/moby/buildkit/solver/pb"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestToResolverOptHTTPChecksumRequest(t *testing.T) {
|
||||||
|
opt := ToResolverOpt(&gwpb.ResolveSourceMetaRequest{
|
||||||
|
Source: &pb.SourceOp{Identifier: "https://example.com/a"},
|
||||||
|
HTTP: &gwpb.ResolveSourceHTTPRequest{
|
||||||
|
ChecksumRequest: &gwpb.ChecksumRequest{
|
||||||
|
Algo: gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA512,
|
||||||
|
Suffix: []byte{0x01, 0x02},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil)
|
||||||
|
require.NotNil(t, opt.HTTPOpt)
|
||||||
|
require.NotNil(t, opt.HTTPOpt.ChecksumReq)
|
||||||
|
require.Equal(t, sourceresolver.ResolveHTTPChecksumAlgoSHA512, opt.HTTPOpt.ChecksumReq.Algo)
|
||||||
|
require.Equal(t, []byte{0x01, 0x02}, opt.HTTPOpt.ChecksumReq.Suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestToGatewayMetaResponseHTTPChecksumResponse(t *testing.T) {
|
||||||
|
out := ToGatewayMetaResponse(&sourceresolver.MetaResponse{
|
||||||
|
Op: &pb.SourceOp{Identifier: "https://example.com/a"},
|
||||||
|
HTTP: &sourceresolver.ResolveHTTPResponse{
|
||||||
|
Digest: "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||||
|
ChecksumResponse: &sourceresolver.ResolveHTTPChecksumResponse{
|
||||||
|
Digest: "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||||
|
Suffix: []byte{0x03, 0x04},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NotNil(t, out.HTTP)
|
||||||
|
require.NotNil(t, out.HTTP.ChecksumResponse)
|
||||||
|
require.Equal(t, "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", out.HTTP.ChecksumResponse.Digest)
|
||||||
|
require.Equal(t, []byte{0x03, 0x04}, out.HTTP.ChecksumResponse.Suffix)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user