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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.5
require (
github.com/Masterminds/semver/v3 v3.4.0
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.7
github.com/compose-spec/compose-go/v2 v2.9.1
github.com/containerd/console v1.0.5
Expand Down Expand Up @@ -78,7 +79,6 @@ require (

require (
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/agnivade/levenshtein v1.2.1 // indirect
github.com/apparentlymart/go-cidr v1.0.1 // indirect
Expand Down
143 changes: 138 additions & 5 deletions policy/funcs.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,24 @@ package policy
import (
"bytes"
"context"
"crypto"
"encoding/json"
"fmt"
"io"
"maps"
"net/url"
"slices"
"strings"

"github.com/distribution/reference"
"github.com/golang/snappy"
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
"github.com/moby/buildkit/client/llb"
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/util/gitutil/gitsign"
"github.com/moby/buildkit/util/pgpsign"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/rego"
"github.com/open-policy-agent/opa/v1/types"
Expand All @@ -26,11 +30,12 @@ import (
)

const (
funcLoadJSON = "load_json"
funcVerifyGitSignature = "verify_git_signature"
funcPinImage = "pin_image"
funcArtifactAttestation = "artifact_attestation"
funcGithubAttestation = "github_attestation"
funcLoadJSON = "load_json"
funcVerifyGitSignature = "verify_git_signature"
funcVerifyHTTPPGPSignature = "verify_http_pgp_signature"
funcPinImage = "pin_image"
funcArtifactAttestation = "artifact_attestation"
funcGithubAttestation = "github_attestation"
)

func (p *Policy) initBuiltinFuncs() {
Expand Down Expand Up @@ -69,6 +74,27 @@ func (p *Policy) initBuiltinFuncs() {
},
})

verifyHTTPPGPSignature := &rego.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 := &rego.Function{
Name: funcPinImage,
Decl: types.NewFunction(
Expand Down Expand Up @@ -482,6 +508,113 @@ func (p *Policy) builtinVerifyGitSignatureImpl(_ rego.BuiltinContext, a1, a2 *as
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) {
if p.opt.FS == nil {
return nil, errors.Errorf("no policy FS defined for reading context files")
Expand Down
183 changes: 183 additions & 0 deletions policy/funcs_http_pgp_test.go
Original file line number Diff line number Diff line change
@@ -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...)
}
Loading
Loading