Skip to content

Commit a26bcfa

Browse files
committed
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>
1 parent 22074cd commit a26bcfa

8 files changed

Lines changed: 467 additions & 15 deletions

File tree

policy/funcs.go

Lines changed: 138 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,24 @@ package policy
33
import (
44
"bytes"
55
"context"
6+
"crypto"
67
"encoding/json"
78
"fmt"
89
"io"
910
"maps"
1011
"net/url"
12+
"slices"
1113
"strings"
1214

1315
"github.com/distribution/reference"
1416
"github.com/golang/snappy"
1517
slsa1 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v1"
1618
"github.com/moby/buildkit/client/llb"
1719
gwclient "github.com/moby/buildkit/frontend/gateway/client"
20+
gwpb "github.com/moby/buildkit/frontend/gateway/pb"
1821
"github.com/moby/buildkit/solver/pb"
1922
"github.com/moby/buildkit/util/gitutil/gitsign"
23+
"github.com/moby/buildkit/util/pgpsign"
2024
"github.com/open-policy-agent/opa/v1/ast"
2125
"github.com/open-policy-agent/opa/v1/rego"
2226
"github.com/open-policy-agent/opa/v1/types"
@@ -26,11 +30,12 @@ import (
2630
)
2731

2832
const (
29-
funcLoadJSON = "load_json"
30-
funcVerifyGitSignature = "verify_git_signature"
31-
funcPinImage = "pin_image"
32-
funcArtifactAttestation = "artifact_attestation"
33-
funcGithubAttestation = "github_attestation"
33+
funcLoadJSON = "load_json"
34+
funcVerifyGitSignature = "verify_git_signature"
35+
funcVerifyHTTPPGPSignature = "verify_http_pgp_signature"
36+
funcPinImage = "pin_image"
37+
funcArtifactAttestation = "artifact_attestation"
38+
funcGithubAttestation = "github_attestation"
3439
)
3540

3641
func (p *Policy) initBuiltinFuncs() {
@@ -69,6 +74,27 @@ func (p *Policy) initBuiltinFuncs() {
6974
},
7075
})
7176

77+
verifyHTTPPGPSignature := &rego.Function{
78+
Name: funcVerifyHTTPPGPSignature,
79+
Decl: types.NewFunction(
80+
types.Args(
81+
types.A,
82+
types.S,
83+
types.S,
84+
),
85+
types.B,
86+
),
87+
Memoize: false, // TODO:optimize
88+
}
89+
p.funcs = append(p.funcs, fun{
90+
decl: verifyHTTPPGPSignature,
91+
impl: func(s *state) func(*rego.Rego) {
92+
return rego.Function3(verifyHTTPPGPSignature, func(bctx rego.BuiltinContext, a1 *ast.Term, a2 *ast.Term, a3 *ast.Term) (*ast.Term, error) {
93+
return p.builtinVerifyHTTPPGPSignatureImpl(bctx, a1, a2, a3, s)
94+
})
95+
},
96+
})
97+
7298
pinImageDigest := &rego.Function{
7399
Name: funcPinImage,
74100
Decl: types.NewFunction(
@@ -482,6 +508,113 @@ func (p *Policy) builtinVerifyGitSignatureImpl(_ rego.BuiltinContext, a1, a2 *as
482508
return ast.BooleanTerm(true), nil
483509
}
484510

511+
func (p *Policy) builtinVerifyHTTPPGPSignatureImpl(_ rego.BuiltinContext, a1, a2, a3 *ast.Term, s *state) (*ast.Term, error) {
512+
inp := s.Input
513+
if inp.HTTP == nil {
514+
return ast.BooleanTerm(false), nil
515+
}
516+
517+
obja, ok := a1.Value.(ast.Object)
518+
if !ok {
519+
return nil, errors.Errorf("%s: expected object, got %T", funcVerifyHTTPPGPSignature, a1.Value)
520+
}
521+
522+
httpValue, err := ast.InterfaceToValue(inp.HTTP)
523+
if err != nil {
524+
return nil, errors.Wrapf(err, "%s: failed converting object to interface", funcVerifyHTTPPGPSignature)
525+
}
526+
527+
if obja.Compare(httpValue) != 0 {
528+
return nil, errors.Errorf("%s: first argument is not the same as input http", funcVerifyHTTPPGPSignature)
529+
}
530+
531+
sigPath, ok := a2.Value.(ast.String)
532+
if !ok {
533+
return nil, errors.Errorf("%s: expected string signature path, got %T", funcVerifyHTTPPGPSignature, a2.Value)
534+
}
535+
pubKeyPath, ok := a3.Value.(ast.String)
536+
if !ok {
537+
return nil, errors.Errorf("%s: expected string pubkey path, got %T", funcVerifyHTTPPGPSignature, a3.Value)
538+
}
539+
540+
signatureData, err := p.readFile(string(sigPath), 512*1024)
541+
if err != nil {
542+
return nil, err
543+
}
544+
pubKeyData, err := p.readFile(string(pubKeyPath), 512*1024)
545+
if err != nil {
546+
return nil, err
547+
}
548+
549+
sig, _, err := pgpsign.ParseArmoredDetachedSignature(signatureData)
550+
if err != nil {
551+
return nil, errors.Wrapf(err, "%s: failed to parse detached signature", funcVerifyHTTPPGPSignature)
552+
}
553+
keyring, err := pgpsign.ReadAllArmoredKeyRings(pubKeyData)
554+
if err != nil {
555+
return nil, errors.Wrapf(err, "%s: failed to read armored keyring", funcVerifyHTTPPGPSignature)
556+
}
557+
558+
algo, err := toPBChecksumAlgo(sig.Hash)
559+
if err != nil {
560+
return nil, errors.Wrapf(err, "%s: unsupported signature hash", funcVerifyHTTPPGPSignature)
561+
}
562+
suffix := slices.Clone(sig.HashSuffix)
563+
checksumReq := &gwpb.ChecksumRequest{Algo: algo, Suffix: suffix}
564+
565+
resp := inp.HTTP.checksumResponseForSignature
566+
if resp == nil || resp.Digest == "" {
567+
s.checksumNeededForSignature = checksumReq
568+
s.addUnknown(funcVerifyHTTPPGPSignature)
569+
return ast.BooleanTerm(false), nil
570+
}
571+
if !bytes.Equal(resp.Suffix, suffix) {
572+
s.checksumNeededForSignature = checksumReq
573+
s.addUnknown(funcVerifyHTTPPGPSignature)
574+
return ast.BooleanTerm(false), nil
575+
}
576+
dgst, err := digest.Parse(resp.Digest)
577+
if err != nil {
578+
return nil, errors.Wrapf(err, "%s: invalid checksum digest", funcVerifyHTTPPGPSignature)
579+
}
580+
if !checksumAlgoMatches(algo, dgst.Algorithm()) {
581+
s.checksumNeededForSignature = checksumReq
582+
s.addUnknown(funcVerifyHTTPPGPSignature)
583+
return ast.BooleanTerm(false), nil
584+
}
585+
586+
if err := pgpsign.VerifySignatureWithDigest(sig, keyring, dgst); err != nil {
587+
return ast.BooleanTerm(false), nil
588+
}
589+
return ast.BooleanTerm(true), nil
590+
}
591+
592+
func toPBChecksumAlgo(hash crypto.Hash) (gwpb.ChecksumRequest_ChecksumAlgo, error) {
593+
switch hash {
594+
case crypto.SHA256:
595+
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA256, nil
596+
case crypto.SHA384:
597+
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA384, nil
598+
case crypto.SHA512:
599+
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA512, nil
600+
default:
601+
return gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA256, errors.Errorf("unsupported signature hash algorithm %v", hash)
602+
}
603+
}
604+
605+
func checksumAlgoMatches(algo gwpb.ChecksumRequest_ChecksumAlgo, digestAlgo digest.Algorithm) bool {
606+
switch algo {
607+
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA256:
608+
return digestAlgo == digest.SHA256
609+
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA384:
610+
return digestAlgo == digest.SHA384
611+
case gwpb.ChecksumRequest_CHECKSUM_ALGO_SHA512:
612+
return digestAlgo == digest.SHA512
613+
default:
614+
return false
615+
}
616+
}
617+
485618
func (p *Policy) readFile(path string, limit int64) ([]byte, error) {
486619
if p.opt.FS == nil {
487620
return nil, errors.Errorf("no policy FS defined for reading context files")

policy/funcs_http_pgp_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package policy
2+
3+
import (
4+
"bytes"
5+
"crypto"
6+
"encoding/hex"
7+
"io/fs"
8+
"testing"
9+
"testing/fstest"
10+
11+
"github.com/ProtonMail/go-crypto/openpgp"
12+
"github.com/ProtonMail/go-crypto/openpgp/armor"
13+
"github.com/ProtonMail/go-crypto/openpgp/packet"
14+
"github.com/moby/buildkit/util/pgpsign"
15+
"github.com/open-policy-agent/opa/v1/ast"
16+
"github.com/open-policy-agent/opa/v1/rego"
17+
digest "github.com/opencontainers/go-digest"
18+
"github.com/stretchr/testify/require"
19+
)
20+
21+
func TestBuiltinVerifyHTTPPGPSignatureImpl(t *testing.T) {
22+
const (
23+
sigPath = "sig.asc"
24+
keyPath = "pubkey.asc"
25+
)
26+
payload := []byte("buildx-http-payload")
27+
sigData, pubKeyData, checksumDigest, suffix := createDetachedPGPFixture(t, payload)
28+
29+
newPolicy := func(sig []byte, pub []byte) *Policy {
30+
return NewPolicy(Opt{
31+
FS: func() (fs.StatFS, func() error, error) {
32+
return fstest.MapFS{
33+
sigPath: &fstest.MapFile{Data: sig},
34+
keyPath: &fstest.MapFile{Data: pub},
35+
}, func() error { return nil }, nil
36+
},
37+
})
38+
}
39+
40+
t.Run("success", func(t *testing.T) {
41+
st := &state{
42+
Input: Input{
43+
HTTP: &HTTP{
44+
checksumResponseForSignature: &httpChecksumResponseForSignature{
45+
Digest: checksumDigest.String(),
46+
Suffix: append([]byte(nil), suffix...),
47+
},
48+
},
49+
},
50+
}
51+
p := newPolicy(sigData, pubKeyData)
52+
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
53+
require.NoError(t, err)
54+
55+
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
56+
rego.BuiltinContext{Context: t.Context()},
57+
ast.NewTerm(httpVal),
58+
ast.StringTerm(sigPath),
59+
ast.StringTerm(keyPath),
60+
st,
61+
)
62+
require.NoError(t, err)
63+
require.Equal(t, ast.BooleanTerm(true), got)
64+
require.Nil(t, st.checksumNeededForSignature)
65+
})
66+
67+
t.Run("verify-failure-returns-false", func(t *testing.T) {
68+
encoded := checksumDigest.Encoded()
69+
require.NotEmpty(t, encoded)
70+
flipped := "0" + encoded[1:]
71+
badDigest := digest.NewDigestFromEncoded(checksumDigest.Algorithm(), flipped)
72+
73+
st := &state{
74+
Input: Input{
75+
HTTP: &HTTP{
76+
checksumResponseForSignature: &httpChecksumResponseForSignature{
77+
Digest: badDigest.String(),
78+
Suffix: append([]byte(nil), suffix...),
79+
},
80+
},
81+
},
82+
}
83+
p := newPolicy(sigData, pubKeyData)
84+
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
85+
require.NoError(t, err)
86+
87+
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
88+
rego.BuiltinContext{Context: t.Context()},
89+
ast.NewTerm(httpVal),
90+
ast.StringTerm(sigPath),
91+
ast.StringTerm(keyPath),
92+
st,
93+
)
94+
require.NoError(t, err)
95+
require.Equal(t, ast.BooleanTerm(false), got)
96+
})
97+
98+
t.Run("missing-checksum-response-returns-false-and-adds-unknown", func(t *testing.T) {
99+
st := &state{
100+
Input: Input{
101+
HTTP: &HTTP{},
102+
},
103+
}
104+
p := newPolicy(sigData, pubKeyData)
105+
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
106+
require.NoError(t, err)
107+
108+
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
109+
rego.BuiltinContext{Context: t.Context()},
110+
ast.NewTerm(httpVal),
111+
ast.StringTerm(sigPath),
112+
ast.StringTerm(keyPath),
113+
st,
114+
)
115+
require.NoError(t, err)
116+
require.Equal(t, ast.BooleanTerm(false), got)
117+
require.Contains(t, st.Unknowns, funcVerifyHTTPPGPSignature)
118+
require.NotNil(t, st.checksumNeededForSignature)
119+
})
120+
121+
t.Run("invalid-signature-errors", func(t *testing.T) {
122+
p := newPolicy([]byte("not-a-signature"), pubKeyData)
123+
st := &state{Input: Input{HTTP: &HTTP{}}}
124+
httpVal, err := ast.InterfaceToValue(st.Input.HTTP)
125+
require.NoError(t, err)
126+
127+
got, err := p.builtinVerifyHTTPPGPSignatureImpl(
128+
rego.BuiltinContext{Context: t.Context()},
129+
ast.NewTerm(httpVal),
130+
ast.StringTerm(sigPath),
131+
ast.StringTerm(keyPath),
132+
st,
133+
)
134+
require.Nil(t, got)
135+
require.ErrorContains(t, err, "verify_http_pgp_signature: failed to parse detached signature")
136+
})
137+
}
138+
139+
func createDetachedPGPFixture(t *testing.T, payload []byte) ([]byte, []byte, digest.Digest, []byte) {
140+
t.Helper()
141+
142+
entity, err := openpgp.NewEntity("buildx", "", "buildx@example.com", &packet.Config{
143+
DefaultHash: crypto.SHA256,
144+
RSABits: 2048,
145+
})
146+
require.NoError(t, err)
147+
148+
var sigBuf bytes.Buffer
149+
err = openpgp.ArmoredDetachSign(&sigBuf, entity, bytes.NewReader(payload), &packet.Config{
150+
DefaultHash: crypto.SHA256,
151+
})
152+
require.NoError(t, err)
153+
sigData := sigBuf.Bytes()
154+
155+
var pubBuf bytes.Buffer
156+
aw, err := armor.Encode(&pubBuf, openpgp.PublicKeyType, nil)
157+
require.NoError(t, err)
158+
err = entity.Serialize(aw)
159+
require.NoError(t, err)
160+
err = aw.Close()
161+
require.NoError(t, err)
162+
pubKeyData := pubBuf.Bytes()
163+
164+
sig, _, err := pgpsign.ParseArmoredDetachedSignature(sigData)
165+
require.NoError(t, err)
166+
167+
h := sig.Hash.New()
168+
_, err = h.Write(payload)
169+
require.NoError(t, err)
170+
_, err = h.Write(sig.HashSuffix)
171+
require.NoError(t, err)
172+
sum := h.Sum(nil)
173+
174+
dAlgo := digest.SHA256
175+
switch sig.Hash {
176+
case crypto.SHA384:
177+
dAlgo = digest.SHA384
178+
case crypto.SHA512:
179+
dAlgo = digest.SHA512
180+
}
181+
182+
return sigData, pubKeyData, digest.NewDigestFromEncoded(dAlgo, hex.EncodeToString(sum)), append([]byte(nil), sig.HashSuffix...)
183+
}

0 commit comments

Comments
 (0)