mirror of
https://github.com/moby/buildkit.git
synced 2026-11-06 05:16:45 +00:00
source/http: add PGP verification and shared pgpsign
Add detached PGP verification for HTTP sources during metadata resolution and expose LLB options/caps/attrs for signature validation. Extract shared OpenPGP verification/parsing logic into util/pgpsign and reuse it from git signing, plus add integration and source-level tests. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
11 files changed
+660
-301
No files matched your search
@@ -120,6 +120,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
|
||||
testBuildHTTPSourceAuthHeaderSecret,
|
||||
testBuildHTTPSourceHostTokenSecret,
|
||||
testBuildHTTPSourceHeader,
|
||||
testBuildHTTPSourcePGPSignatureVerify,
|
||||
testBuildPushAndValidate,
|
||||
testBuildExportWithUncompressed,
|
||||
testBuildExportScratch,
|
||||
@@ -3616,6 +3617,93 @@ func testBuildHTTPSourceHeader(t *testing.T, sb integration.Sandbox) {
|
||||
require.Equal(t, "fooagent", allReqs[0].Header.Get("user-agent"))
|
||||
}
|
||||
|
||||
func testBuildHTTPSourcePGPSignatureVerify(t *testing.T, sb integration.Sandbox) {
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
require.NoError(t, err)
|
||||
defer c.Close()
|
||||
|
||||
signFixturesPath, ok := os.LookupEnv("BUILDKIT_TEST_SIGN_FIXTURES")
|
||||
if !ok {
|
||||
t.Skip("missing BUILDKIT_TEST_SIGN_FIXTURES")
|
||||
}
|
||||
|
||||
payload, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.http.artifact"))
|
||||
require.NoError(t, err)
|
||||
sigData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.http.artifact.asc"))
|
||||
require.NoError(t, err)
|
||||
pubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.gpg.pub"))
|
||||
require.NoError(t, err)
|
||||
wrongPubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user2.gpg.pub"))
|
||||
require.NoError(t, err)
|
||||
|
||||
httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/artifact.txt" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
_, _ = w.Write(payload)
|
||||
}))
|
||||
defer httpSrv.Close()
|
||||
|
||||
solve := func(t *testing.T, state llb.State) error {
|
||||
def, err := state.Marshal(sb.Context())
|
||||
require.NoError(t, err)
|
||||
tmpdir := t.TempDir()
|
||||
_, err = c.Solve(sb.Context(), def, SolveOpt{
|
||||
Exports: []ExportEntry{
|
||||
{
|
||||
Type: ExporterLocal,
|
||||
OutputDir: tmpdir,
|
||||
},
|
||||
},
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dt, err := os.ReadFile(filepath.Join(tmpdir, "artifact.txt"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, payload, dt)
|
||||
return nil
|
||||
}
|
||||
|
||||
t.Run("valid-signature", func(t *testing.T) {
|
||||
validState := llb.HTTP(
|
||||
httpSrv.URL+"/artifact.txt",
|
||||
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
|
||||
PubKey: pubKeyData,
|
||||
Signature: sigData,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, solve(t, validState))
|
||||
})
|
||||
|
||||
t.Run("wrong-pubkey", func(t *testing.T) {
|
||||
invalidState := llb.HTTP(
|
||||
httpSrv.URL+"/artifact.txt",
|
||||
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
|
||||
PubKey: wrongPubKeyData,
|
||||
Signature: sigData,
|
||||
}),
|
||||
)
|
||||
err = solve(t, invalidState)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to verify pgp signature")
|
||||
})
|
||||
|
||||
t.Run("concatenated-pubkeys-right-key-second", func(t *testing.T) {
|
||||
mergedPubKeys := append(append([]byte{}, wrongPubKeyData...), '\n')
|
||||
mergedPubKeys = append(mergedPubKeys, pubKeyData...)
|
||||
state := llb.HTTP(
|
||||
httpSrv.URL+"/artifact.txt",
|
||||
llb.VerifyPGPSignature(llb.HTTPSignatureInfo{
|
||||
PubKey: mergedPubKeys,
|
||||
Signature: sigData,
|
||||
}),
|
||||
)
|
||||
require.NoError(t, solve(t, state))
|
||||
})
|
||||
}
|
||||
|
||||
func testResolveAndHosts(t *testing.T, sb integration.Sandbox) {
|
||||
requiresLinux(t)
|
||||
c, err := New(sb.Context(), sb.Address())
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -705,12 +706,30 @@ func HTTP(url string, opts ...HTTPOption) State {
|
||||
hi.Header.setAttrs(attrs)
|
||||
addCap(&hi.Constraints, pb.CapSourceHTTPHeader)
|
||||
}
|
||||
if hi.Signature != nil {
|
||||
if len(hi.Signature.PubKey) > 0 {
|
||||
attrs[pb.AttrHTTPSignatureVerifyPubKey] = string(hi.Signature.PubKey)
|
||||
}
|
||||
if len(hi.Signature.Signature) > 0 {
|
||||
attrs[pb.AttrHTTPSignatureVerify] = string(hi.Signature.Signature)
|
||||
}
|
||||
addCap(&hi.Constraints, pb.CapSourceHTTPSignatureVerify)
|
||||
}
|
||||
|
||||
addCap(&hi.Constraints, pb.CapSourceHTTP)
|
||||
source := NewSource(url, attrs, hi.Constraints)
|
||||
return NewState(source.Output())
|
||||
}
|
||||
|
||||
// HTTPSignatureInfo configures detached-signature verification for HTTP
|
||||
// sources. The current implementation uses inline armored signatures.
|
||||
type HTTPSignatureInfo struct {
|
||||
PubKey []byte
|
||||
|
||||
// Signature is an inline detached armored OpenPGP signature.
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
type HTTPInfo struct {
|
||||
constraintsWrapper
|
||||
Checksum digest.Digest
|
||||
@@ -720,6 +739,7 @@ type HTTPInfo struct {
|
||||
GID int
|
||||
AuthHeaderSecret string
|
||||
Header *HTTPHeader
|
||||
Signature *HTTPSignatureInfo
|
||||
}
|
||||
|
||||
type HTTPOption interface {
|
||||
@@ -757,6 +777,17 @@ func Chown(uid, gid int) HTTPOption {
|
||||
})
|
||||
}
|
||||
|
||||
// VerifyPGPSignature returns an [HTTPOption] for detached OpenPGP signature
|
||||
// verification of the downloaded HTTP payload.
|
||||
func VerifyPGPSignature(info HTTPSignatureInfo) HTTPOption {
|
||||
return httpOptionFunc(func(hi *HTTPInfo) {
|
||||
hi.Signature = &HTTPSignatureInfo{
|
||||
PubKey: slices.Clone(info.PubKey),
|
||||
Signature: slices.Clone(info.Signature),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Header returns an [HTTPOption] that ensures additional request headers will
|
||||
// be sent when retrieving the HTTP source.
|
||||
func Header(header HTTPHeader) HTTPOption {
|
||||
|
||||
+20
-92
@@ -1,12 +1,10 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"hash"
|
||||
@@ -20,8 +18,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/containerd/platforms"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/moby/buildkit/client/llb"
|
||||
@@ -31,7 +27,7 @@ import (
|
||||
opspb "github.com/moby/buildkit/solver/pb"
|
||||
sourcepolicypb "github.com/moby/buildkit/sourcepolicy/pb"
|
||||
"github.com/moby/buildkit/sourcepolicy/policysession"
|
||||
"github.com/moby/buildkit/util/gitutil/gitsign"
|
||||
"github.com/moby/buildkit/util/pgpsign"
|
||||
"github.com/moby/buildkit/util/testutil/integration"
|
||||
"github.com/moby/buildkit/util/testutil/workers"
|
||||
policyimage "github.com/moby/policy-helpers/image"
|
||||
@@ -934,10 +930,9 @@ func testSourcePolicySessionHTTPChecksumAssist(t *testing.T, sb integration.Sand
|
||||
require.NoError(t, err)
|
||||
pubKeyData, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.gpg.pub"))
|
||||
require.NoError(t, err)
|
||||
sig, err := gitsign.ParseSignature(sigData)
|
||||
sig, _, err := pgpsign.ParseArmoredDetachedSignature(sigData)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, sig.PGPSignature)
|
||||
keyring, err := openpgp.ReadArmoredKeyRing(bytes.NewReader(pubKeyData))
|
||||
keyring, err := pgpsign.ReadAllArmoredKeyRings(pubKeyData)
|
||||
require.NoError(t, err)
|
||||
|
||||
httpSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -953,9 +948,9 @@ func testSourcePolicySessionHTTPChecksumAssist(t *testing.T, sb integration.Sand
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("valid checksum request", func(t *testing.T) {
|
||||
algo, err := toPBChecksumAlgo(sig.PGPSignature.Hash)
|
||||
algo, err := toPBChecksumAlgo(sig.Hash)
|
||||
require.NoError(t, err)
|
||||
expectedDigest, err := payloadWithSuffixDigest(sig.PGPSignature.Hash, payload, sig.PGPSignature.HashSuffix)
|
||||
expectedDigest, err := payloadWithSuffixDigest(sig.Hash, payload, sig.HashSuffix)
|
||||
require.NoError(t, err)
|
||||
|
||||
callCounter := 0
|
||||
@@ -969,7 +964,7 @@ func testSourcePolicySessionHTTPChecksumAssist(t *testing.T, sb integration.Sand
|
||||
HTTP: &pb.ResolveSourceHTTPRequest{
|
||||
ChecksumRequest: &pb.ChecksumRequest{
|
||||
Algo: algo,
|
||||
Suffix: slices.Clone(sig.PGPSignature.HashSuffix),
|
||||
Suffix: slices.Clone(sig.HashSuffix),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
@@ -978,11 +973,13 @@ func testSourcePolicySessionHTTPChecksumAssist(t *testing.T, sb integration.Sand
|
||||
require.NotNil(t, req.Source.HTTP)
|
||||
require.NotNil(t, req.Source.HTTP.ChecksumResponse)
|
||||
require.Equal(t, expectedDigest, req.Source.HTTP.ChecksumResponse.Digest)
|
||||
require.Equal(t, sig.PGPSignature.HashSuffix, req.Source.HTTP.ChecksumResponse.Suffix)
|
||||
require.NoError(t, verifySignatureWithChecksumDigest(sig.PGPSignature, keyring, req.Source.HTTP.ChecksumResponse.Digest))
|
||||
require.Equal(t, sig.HashSuffix, req.Source.HTTP.ChecksumResponse.Suffix)
|
||||
responseDigest, err := digest.Parse(req.Source.HTTP.ChecksumResponse.Digest)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, pgpsign.VerifySignatureWithDigest(sig, keyring, responseDigest))
|
||||
// Negative check: tampered digest must fail signature verification.
|
||||
badDigest := tamperDigestHex(req.Source.HTTP.ChecksumResponse.Digest)
|
||||
err := verifySignatureWithChecksumDigest(sig.PGPSignature, keyring, badDigest)
|
||||
badDigest := tamperDigestHex(responseDigest)
|
||||
err = pgpsign.VerifySignatureWithDigest(sig, keyring, badDigest)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to verify signature with checksum digest")
|
||||
return &policysession.DecisionResponse{
|
||||
@@ -1089,84 +1086,15 @@ func payloadWithSuffixDigest(algo crypto.Hash, payload, suffix []byte) (string,
|
||||
return fmt.Sprintf("%s:%x", algoName, h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func verifySignatureWithChecksumDigest(sig *packet.Signature, keyring openpgp.EntityList, digestStr string) error {
|
||||
algoPart, hexPart, ok := strings.Cut(digestStr, ":")
|
||||
if !ok {
|
||||
return errors.Errorf("invalid digest format %q", digestStr)
|
||||
func tamperDigestHex(dgst digest.Digest) digest.Digest {
|
||||
hexPart := []byte(dgst.Encoded())
|
||||
if len(hexPart) == 0 {
|
||||
return dgst
|
||||
}
|
||||
|
||||
algoName, err := checksumAlgoName(sig.Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if algoPart != algoName {
|
||||
return errors.Errorf("digest algorithm mismatch: %s != %s", algoPart, algoName)
|
||||
}
|
||||
sum, err := hex.DecodeString(hexPart)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid digest hex")
|
||||
}
|
||||
|
||||
h := &staticHash{sum: sum}
|
||||
// simplistic verification only for testing purposes
|
||||
for _, e := range keyring {
|
||||
if e.PrimaryKey != nil && e.PrimaryKey.VerifySignature(h, sig) == nil {
|
||||
return nil
|
||||
}
|
||||
for _, sub := range e.Subkeys {
|
||||
if sub.PublicKey != nil && sub.PublicKey.VerifySignature(h, sig) == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return errors.New("failed to verify signature with checksum digest")
|
||||
}
|
||||
|
||||
func tamperDigestHex(digestStr string) string {
|
||||
algoPart, hexPart, ok := strings.Cut(digestStr, ":")
|
||||
if !ok || hexPart == "" {
|
||||
return digestStr
|
||||
}
|
||||
h := []byte(hexPart)
|
||||
if h[len(h)-1] == '0' {
|
||||
h[len(h)-1] = '1'
|
||||
if hexPart[len(hexPart)-1] == '0' {
|
||||
hexPart[len(hexPart)-1] = '1'
|
||||
} else {
|
||||
h[len(h)-1] = '0'
|
||||
hexPart[len(hexPart)-1] = '0'
|
||||
}
|
||||
return algoPart + ":" + string(h)
|
||||
}
|
||||
|
||||
func checksumAlgoName(in crypto.Hash) (string, error) {
|
||||
switch in {
|
||||
case crypto.SHA256:
|
||||
return "sha256", nil
|
||||
case crypto.SHA384:
|
||||
return "sha384", nil
|
||||
case crypto.SHA512:
|
||||
return "sha512", nil
|
||||
default:
|
||||
return "", errors.Errorf("unsupported signature hash algorithm %v", in)
|
||||
}
|
||||
}
|
||||
|
||||
type staticHash struct {
|
||||
sum []byte
|
||||
}
|
||||
|
||||
func (s *staticHash) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (s *staticHash) Sum(b []byte) []byte {
|
||||
return append(b, s.sum...)
|
||||
}
|
||||
|
||||
func (s *staticHash) Reset() {}
|
||||
|
||||
func (s *staticHash) Size() int {
|
||||
return len(s.sum)
|
||||
}
|
||||
|
||||
func (s *staticHash) BlockSize() int {
|
||||
return len(s.sum)
|
||||
return digest.NewDigestFromEncoded(dgst.Algorithm(), string(hexPart))
|
||||
}
|
||||
@@ -32,6 +32,8 @@ const AttrHTTPUID = "http.uid"
|
||||
const AttrHTTPGID = "http.gid"
|
||||
const AttrHTTPAuthHeaderSecret = "http.authheadersecret"
|
||||
const AttrHTTPHeaderPrefix = "http.header."
|
||||
const AttrHTTPSignatureVerifyPubKey = "http.sig.pubkey"
|
||||
const AttrHTTPSignatureVerify = "http.sig.signature"
|
||||
|
||||
const AttrImageResolveMode = "image.resolvemode"
|
||||
const AttrImageResolveModeDefault = "default"
|
||||
|
||||
+9
-2
@@ -40,8 +40,9 @@ const (
|
||||
CapSourceHTTPChecksum apicaps.CapID = "source.http.checksum"
|
||||
CapSourceHTTPPerm apicaps.CapID = "source.http.perm"
|
||||
// NOTE the historical typo
|
||||
CapSourceHTTPUIDGID apicaps.CapID = "soruce.http.uidgid"
|
||||
CapSourceHTTPHeader apicaps.CapID = "source.http.header"
|
||||
CapSourceHTTPUIDGID apicaps.CapID = "soruce.http.uidgid"
|
||||
CapSourceHTTPHeader apicaps.CapID = "source.http.header"
|
||||
CapSourceHTTPSignatureVerify apicaps.CapID = "source.http.signatureverify"
|
||||
|
||||
CapSourceOCILayout apicaps.CapID = "source.ocilayout"
|
||||
|
||||
@@ -288,6 +289,12 @@ func init() {
|
||||
Status: apicaps.CapStatusExperimental,
|
||||
})
|
||||
|
||||
Caps.Init(apicaps.Cap{
|
||||
ID: CapSourceHTTPSignatureVerify,
|
||||
Enabled: true,
|
||||
Status: apicaps.CapStatusExperimental,
|
||||
})
|
||||
|
||||
Caps.Init(apicaps.Cap{
|
||||
ID: CapSourceOCILayout,
|
||||
Enabled: true,
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/moby/buildkit/util/gitutil"
|
||||
"github.com/moby/buildkit/util/gitutil/gitobject"
|
||||
"github.com/moby/buildkit/util/gitutil/gitsign"
|
||||
"github.com/moby/buildkit/util/pgpsign"
|
||||
"github.com/moby/buildkit/util/progress/logs"
|
||||
"github.com/moby/buildkit/util/urlutil"
|
||||
"github.com/moby/locker"
|
||||
@@ -308,7 +309,7 @@ func verifyGitSignature(md *Metadata, opts *GitSignatureVerifyOptions) error {
|
||||
if err := tagObj.VerifyChecksum(md.Checksum); err != nil {
|
||||
return errors.Wrap(err, "tag object checksum verification failed")
|
||||
}
|
||||
tagVerifyError = gitsign.VerifySignature(tagObj, opts.PubKey, &gitsign.VerifyPolicy{
|
||||
tagVerifyError = gitsign.VerifySignature(tagObj, opts.PubKey, &pgpsign.VerifyPolicy{
|
||||
RejectExpiredKeys: opts.RejectExpiredKeys,
|
||||
})
|
||||
if tagVerifyError == nil {
|
||||
@@ -333,7 +334,7 @@ func verifyGitSignature(md *Metadata, opts *GitSignatureVerifyOptions) error {
|
||||
if err := commitObj.VerifyChecksum(expected); err != nil {
|
||||
return errors.Wrap(err, "commit object checksum verification failed")
|
||||
}
|
||||
return gitsign.VerifySignature(commitObj, opts.PubKey, &gitsign.VerifyPolicy{
|
||||
return gitsign.VerifySignature(commitObj, opts.PubKey, &pgpsign.VerifyPolicy{
|
||||
RejectExpiredKeys: opts.RejectExpiredKeys,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,6 +27,12 @@ type HTTPIdentifier struct {
|
||||
GID int
|
||||
AuthHeaderSecret string
|
||||
Header []HeaderField
|
||||
VerifySignature *HTTPSignatureVerifyOptions
|
||||
}
|
||||
|
||||
type HTTPSignatureVerifyOptions struct {
|
||||
PubKey []byte
|
||||
Signature []byte
|
||||
}
|
||||
|
||||
type HeaderField struct {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
srctypes "github.com/moby/buildkit/source/types"
|
||||
"github.com/moby/buildkit/util/bklog"
|
||||
"github.com/moby/buildkit/util/cachedigest"
|
||||
"github.com/moby/buildkit/util/pgpsign"
|
||||
"github.com/moby/buildkit/util/tracing"
|
||||
"github.com/moby/buildkit/version"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
@@ -111,6 +112,16 @@ func (hs *Source) Identifier(scheme, ref string, attrs map[string]string, platfo
|
||||
id.GID = int(i)
|
||||
case pb.AttrHTTPAuthHeaderSecret:
|
||||
id.AuthHeaderSecret = v
|
||||
case pb.AttrHTTPSignatureVerifyPubKey:
|
||||
if id.VerifySignature == nil {
|
||||
id.VerifySignature = &HTTPSignatureVerifyOptions{}
|
||||
}
|
||||
id.VerifySignature.PubKey = []byte(v)
|
||||
case pb.AttrHTTPSignatureVerify:
|
||||
if id.VerifySignature == nil {
|
||||
id.VerifySignature = &HTTPSignatureVerifyOptions{}
|
||||
}
|
||||
id.VerifySignature.Signature = []byte(v)
|
||||
default:
|
||||
if name, found := strings.CutPrefix(k, pb.AttrHTTPHeaderPrefix); found {
|
||||
name = http.CanonicalHeaderKey(name)
|
||||
@@ -127,6 +138,10 @@ func (hs *Source) Identifier(scheme, ref string, attrs map[string]string, platfo
|
||||
return cmp.Compare(a.Name, b.Name)
|
||||
})
|
||||
|
||||
if err := validateSignatureVerifyOptions(id.VerifySignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
@@ -258,10 +273,17 @@ func (hs *httpSourceHandler) resolveMetadata(ctx context.Context, jobCtx solver.
|
||||
if err := validateChecksumRequest(opt.ChecksumReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateSignatureVerifyOptions(hs.src.VerifySignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md, err := hs.resolveMetadataStatic(ctx, jobCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
md, err = hs.handleSignatureVerification(ctx, jobCtx, md)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return hs.handleChecksumRequest(ctx, jobCtx, md, opt)
|
||||
}
|
||||
|
||||
@@ -550,6 +572,36 @@ func validateChecksumRequest(req *MetadataChecksumRequest) error {
|
||||
}
|
||||
}
|
||||
|
||||
func validateSignatureVerifyOptions(opt *HTTPSignatureVerifyOptions) error {
|
||||
if opt == nil {
|
||||
return nil
|
||||
}
|
||||
hasPubKey := len(opt.PubKey) > 0
|
||||
hasSignature := len(opt.Signature) > 0
|
||||
if hasPubKey == hasSignature {
|
||||
return nil
|
||||
}
|
||||
return errors.New("http signature verification requires both pubkey and signature")
|
||||
}
|
||||
|
||||
func (hs *httpSourceHandler) handleSignatureVerification(ctx context.Context, jobCtx solver.JobContext, in *Metadata) (*Metadata, error) {
|
||||
out := *in
|
||||
if hs.src.VerifySignature == nil {
|
||||
return &out, nil
|
||||
}
|
||||
if hs.resolved == nil || hs.resolved.refID == "" {
|
||||
md, err := hs.resolveMetadataRef(ctx, jobCtx)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to resolve source reference for signature verification")
|
||||
}
|
||||
out = *md
|
||||
}
|
||||
if err := hs.verifySignature(ctx, out.Filename, hs.resolved.refID, hs.src.VerifySignature); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (hs *httpSourceHandler) handleChecksumRequest(ctx context.Context, jobCtx solver.JobContext, in *Metadata, opt MetadataOpts) (*Metadata, error) {
|
||||
out := *in
|
||||
if opt.ChecksumReq == nil {
|
||||
@@ -571,6 +623,37 @@ func (hs *httpSourceHandler) handleChecksumRequest(ctx context.Context, jobCtx s
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (hs *httpSourceHandler) verifySignature(ctx context.Context, filename, refID string, opts *HTTPSignatureVerifyOptions) error {
|
||||
ref, err := hs.cache.Get(ctx, refID, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to load http source reference")
|
||||
}
|
||||
defer ref.Release(context.WithoutCancel(ctx))
|
||||
|
||||
mount, err := ref.Mount(ctx, false, nil)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to mount http source reference")
|
||||
}
|
||||
lm := snapshot.LocalMounter(mount)
|
||||
dir, err := lm.Mount()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to mount local http source reference")
|
||||
}
|
||||
defer lm.Unmount()
|
||||
|
||||
fp := filepath.Join(dir, filepath.Base(filepath.Join("/", filename)))
|
||||
f, err := os.Open(fp)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to open http source payload")
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if err := pgpsign.VerifyArmoredDetachedSignature(f, opts.Signature, opts.PubKey, nil); err != nil {
|
||||
return errors.Wrap(err, "failed to verify pgp signature")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (hs *httpSourceHandler) computeChecksumResponse(ctx context.Context, filename, refID string, req *MetadataChecksumRequest) (*MetadataChecksumResponse, error) {
|
||||
algo, err := checksumAlgo(req.Algo)
|
||||
if err != nil {
|
||||
|
||||
@@ -29,6 +29,8 @@ import (
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
const signFixturesPathEnv = "BUILDKIT_TEST_SIGN_FIXTURES"
|
||||
|
||||
func TestHTTPSource(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
@@ -296,6 +298,91 @@ func TestHTTPChecksum(t *testing.T) {
|
||||
ref = nil
|
||||
}
|
||||
|
||||
func TestHTTPSignatureVerification(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
|
||||
fixturesPath, ok := os.LookupEnv(signFixturesPathEnv)
|
||||
if !ok {
|
||||
t.Skip("missing BUILDKIT_TEST_SIGN_FIXTURES")
|
||||
}
|
||||
|
||||
payload := readSignFixture(t, fixturesPath, "user1.http.artifact")
|
||||
signature := readSignFixture(t, fixturesPath, "user1.http.artifact.asc")
|
||||
pubKeyUser1 := readSignFixture(t, fixturesPath, "user1.gpg.pub")
|
||||
pubKeyUser2 := readSignFixture(t, fixturesPath, "user2.gpg.pub")
|
||||
|
||||
hs, err := newHTTPSource(t)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httpserver.NewTestServer(map[string]*httpserver.Response{
|
||||
"/artifact": {
|
||||
Etag: identity.NewID(),
|
||||
Content: payload,
|
||||
},
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
t.Run("valid-signature", func(t *testing.T) {
|
||||
id := &HTTPIdentifier{
|
||||
URL: server.URL + "/artifact",
|
||||
VerifySignature: &HTTPSignatureVerifyOptions{
|
||||
PubKey: pubKeyUser1,
|
||||
Signature: signature,
|
||||
},
|
||||
}
|
||||
h, err := hs.Resolve(ctx, id, nil, nil)
|
||||
require.NoError(t, err)
|
||||
_, _, _, _, err = h.CacheKey(ctx, nil, 0)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("wrong-pubkey", func(t *testing.T) {
|
||||
id := &HTTPIdentifier{
|
||||
URL: server.URL + "/artifact",
|
||||
VerifySignature: &HTTPSignatureVerifyOptions{
|
||||
PubKey: pubKeyUser2,
|
||||
Signature: signature,
|
||||
},
|
||||
}
|
||||
h, err := hs.Resolve(ctx, id, nil, nil)
|
||||
require.NoError(t, err)
|
||||
_, _, _, _, err = h.CacheKey(ctx, nil, 0)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "failed to verify pgp signature")
|
||||
})
|
||||
|
||||
t.Run("multiblock-pubkey", func(t *testing.T) {
|
||||
merged := append(append([]byte{}, pubKeyUser2...), '\n')
|
||||
merged = append(merged, pubKeyUser1...)
|
||||
id := &HTTPIdentifier{
|
||||
URL: server.URL + "/artifact",
|
||||
VerifySignature: &HTTPSignatureVerifyOptions{
|
||||
PubKey: merged,
|
||||
Signature: signature,
|
||||
},
|
||||
}
|
||||
h, err := hs.Resolve(ctx, id, nil, nil)
|
||||
require.NoError(t, err)
|
||||
_, _, _, _, err = h.CacheKey(ctx, nil, 0)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("missing-signature", func(t *testing.T) {
|
||||
id := &HTTPIdentifier{
|
||||
URL: server.URL + "/artifact",
|
||||
VerifySignature: &HTTPSignatureVerifyOptions{
|
||||
PubKey: pubKeyUser1,
|
||||
},
|
||||
}
|
||||
h, err := hs.Resolve(ctx, id, nil, nil)
|
||||
require.NoError(t, err)
|
||||
_, _, _, _, err = h.CacheKey(ctx, nil, 0)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "requires both pubkey and signature")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPruneAfterCacheKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.TODO()
|
||||
@@ -511,3 +598,16 @@ func (s *simpleJobContext) Release() error {
|
||||
func (s *simpleJobContext) ResolverCache() solver.ResolverCache {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readSignFixture(t *testing.T, fixturesPath, name string) []byte {
|
||||
t.Helper()
|
||||
p := filepath.Join(fixturesPath, name)
|
||||
dt, err := os.ReadFile(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Skipf("missing signing fixture at %s", p)
|
||||
}
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return dt
|
||||
}
|
||||
+23
-205
@@ -2,39 +2,23 @@ package gitsign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"encoding/pem"
|
||||
"io"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
"github.com/hiddeco/sshsig"
|
||||
"github.com/moby/buildkit/util/gitutil/gitobject"
|
||||
"github.com/moby/buildkit/util/pgpsign"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type sigType int
|
||||
|
||||
const (
|
||||
sigTypePGP sigType = iota
|
||||
sigTypeSSH
|
||||
)
|
||||
|
||||
type Signature struct {
|
||||
PGPSignature *packet.Signature
|
||||
SSHSignature *sshsig.Signature
|
||||
}
|
||||
|
||||
type VerifyPolicy struct {
|
||||
RejectExpiredKeys bool
|
||||
}
|
||||
|
||||
func VerifySignature(obj *gitobject.GitObject, pubKeyData []byte, policy *VerifyPolicy) error {
|
||||
func VerifySignature(obj *gitobject.GitObject, pubKeyData []byte, policy *pgpsign.VerifyPolicy) error {
|
||||
if len(obj.Signature) == 0 {
|
||||
return errors.New("git object is not signed")
|
||||
}
|
||||
@@ -44,91 +28,20 @@ func VerifySignature(obj *gitobject.GitObject, pubKeyData []byte, policy *Verify
|
||||
return err
|
||||
}
|
||||
if s.PGPSignature != nil {
|
||||
return verifyPGPSignature(obj, s.PGPSignature, pubKeyData, policy)
|
||||
return verifyPGPSignature(obj, pubKeyData, policy)
|
||||
} else if s.SSHSignature != nil {
|
||||
return verifySSHSignature(obj, s.SSHSignature, pubKeyData)
|
||||
}
|
||||
return errors.New("no valid signature found")
|
||||
}
|
||||
|
||||
func verifyPGPSignature(obj *gitobject.GitObject, sig *packet.Signature, pubKeyData []byte, policy *VerifyPolicy) error {
|
||||
sigBlock, _, err := parseSignatureBlock([]byte(obj.Signature))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ents, err := readAllArmoredKeyRings(pubKeyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add addition algorithm constraints
|
||||
if err := checkAlgoPolicy(sig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &packet.Config{}
|
||||
if policy == nil || !policy.RejectExpiredKeys {
|
||||
config.Time = func() time.Time {
|
||||
return sig.CreationTime
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := openpgp.CheckDetachedSignature(
|
||||
ents,
|
||||
func verifyPGPSignature(obj *gitobject.GitObject, pubKeyData []byte, policy *pgpsign.VerifyPolicy) error {
|
||||
return pgpsign.VerifyArmoredDetachedSignature(
|
||||
bytes.NewReader([]byte(obj.SignedData)),
|
||||
bytes.NewReader(sigBlock),
|
||||
config,
|
||||
[]byte(obj.Signature),
|
||||
pubKeyData,
|
||||
policy,
|
||||
)
|
||||
if err != nil {
|
||||
if sig.IssuerKeyId != nil {
|
||||
return errors.Wrapf(err, "signature by %X", *sig.IssuerKeyId)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkEntityUsableForSigning(signer, time.Now(), policy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkCreationTime(sig.CreationTime, time.Now()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// readAllArmoredKeyRings parses one or more concatenated armored OpenPGP key
|
||||
// blocks and returns a combined entity list for signature verification.
|
||||
func readAllArmoredKeyRings(pubKeyData []byte) (openpgp.EntityList, error) {
|
||||
var ents openpgp.EntityList
|
||||
r := bytes.NewReader(pubKeyData)
|
||||
|
||||
for {
|
||||
block, err := armor.Decode(r)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode armored public key")
|
||||
}
|
||||
|
||||
if block.Type != openpgp.PublicKeyType && block.Type != openpgp.PrivateKeyType {
|
||||
return nil, errors.Errorf("expected public or private key block, got: %s", block.Type)
|
||||
}
|
||||
|
||||
el, err := openpgp.ReadKeyRing(block.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read armored public key")
|
||||
}
|
||||
ents = append(ents, el...)
|
||||
}
|
||||
|
||||
if len(ents) == 0 {
|
||||
return nil, errors.New("failed to read armored public key: no armored data found")
|
||||
}
|
||||
|
||||
return ents, nil
|
||||
}
|
||||
|
||||
func verifySSHSignature(obj *gitobject.GitObject, sig *sshsig.Signature, pubKeyData []byte) error {
|
||||
@@ -158,130 +71,35 @@ func verifySSHSignature(obj *gitobject.GitObject, sig *sshsig.Signature, pubKeyD
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEntityUsableForSigning(e *openpgp.Entity, now time.Time, policy *VerifyPolicy) error {
|
||||
if e == nil || e.PrimaryKey == nil {
|
||||
return errors.New("nil entity or key")
|
||||
}
|
||||
|
||||
// Expiry
|
||||
if policy != nil && policy.RejectExpiredKeys {
|
||||
if id := e.PrimaryIdentity(); id != nil && id.SelfSignature != nil {
|
||||
if exp := id.SelfSignature.KeyLifetimeSecs; exp != nil && *exp > 0 {
|
||||
expiry := e.PrimaryKey.CreationTime.Add(time.Duration(*exp) * time.Second)
|
||||
if now.After(expiry) {
|
||||
return errors.Errorf("key expired at %v", expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Revocation
|
||||
if err := checkEntityRevocation(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// RSA bit length (optional)
|
||||
if rsaPub, ok := e.PrimaryKey.PublicKey.(*rsa.PublicKey); ok {
|
||||
if rsaPub.N.BitLen() < 2048 {
|
||||
return errors.Errorf("RSA key too short: %d bits", rsaPub.N.BitLen())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEntityRevocation(e *openpgp.Entity) error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
for _, r := range e.Revocations {
|
||||
if r == nil || r.SigType != packet.SigTypeKeyRevocation {
|
||||
continue
|
||||
}
|
||||
if err := e.PrimaryKey.VerifyRevocationSignature(r); err != nil {
|
||||
continue // ignore malformed or unverified revocations
|
||||
}
|
||||
if r.RevocationReasonText != "" {
|
||||
return errors.Errorf("key revoked: %s", r.RevocationReasonText)
|
||||
}
|
||||
return errors.New("key revoked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseSignatureBlock(data []byte) ([]byte, sigType, error) {
|
||||
func parseSignatureBlock(data []byte) ([]byte, error) {
|
||||
if strings.HasPrefix(string(data), "-----BEGIN SSH SIGNATURE-----") {
|
||||
block, _ := pem.Decode(data)
|
||||
if block == nil || block.Type != "SSH SIGNATURE" {
|
||||
return nil, 0, errors.New("failed to decode ssh signature PEM block")
|
||||
return nil, errors.New("failed to decode ssh signature PEM block")
|
||||
}
|
||||
return block.Bytes, sigTypeSSH, nil
|
||||
} else if strings.HasPrefix(string(data), "-----BEGIN PGP SIGNATURE-----") {
|
||||
block, err := armor.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to decode armored signature")
|
||||
}
|
||||
dt, err := io.ReadAll(block.Body)
|
||||
if err != nil {
|
||||
return nil, 0, errors.Wrap(err, "failed to read armored signature body")
|
||||
}
|
||||
return dt, sigTypePGP, nil
|
||||
return block.Bytes, nil
|
||||
}
|
||||
return nil, 0, errors.Errorf("invalid signature format")
|
||||
return nil, errors.Errorf("invalid signature format")
|
||||
}
|
||||
|
||||
func ParseSignature(data []byte) (*Signature, error) {
|
||||
sigBlock, typ, err := parseSignatureBlock(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if strings.HasPrefix(string(data), "-----BEGIN PGP SIGNATURE-----") {
|
||||
sig, _, err := pgpsign.ParseArmoredDetachedSignature(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Signature{PGPSignature: sig}, nil
|
||||
}
|
||||
switch typ {
|
||||
case sigTypePGP:
|
||||
pr := packet.NewReader(bytes.NewReader(sigBlock))
|
||||
for {
|
||||
p, err := pr.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read next packet")
|
||||
}
|
||||
sig, ok := p.(*packet.Signature)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return &Signature{PGPSignature: sig}, nil
|
||||
if strings.HasPrefix(string(data), "-----BEGIN SSH SIGNATURE-----") {
|
||||
sigBlock, err := parseSignatureBlock(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case sigTypeSSH:
|
||||
sig, err := sshsig.ParseSignature(sigBlock)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to parse ssh signature")
|
||||
}
|
||||
return &Signature{SSHSignature: sig}, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("no signature packet found")
|
||||
}
|
||||
|
||||
func checkAlgoPolicy(sig *packet.Signature) error {
|
||||
switch sig.Hash {
|
||||
case crypto.SHA256, crypto.SHA384, crypto.SHA512:
|
||||
// ok
|
||||
default:
|
||||
return errors.Errorf("rejecting weak/unknown hash: %v", sig.Hash)
|
||||
}
|
||||
// Pubkey policy
|
||||
switch sig.PubKeyAlgo {
|
||||
case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA, packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSASignOnly:
|
||||
default:
|
||||
return errors.Errorf("rejecting unsupported pubkey algorithm: %v", sig.PubKeyAlgo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCreationTime(sigTime, now time.Time) error {
|
||||
if sigTime.After(now.Add(5 * time.Minute)) {
|
||||
return errors.Errorf("signature creation time is in the future: %v", sigTime)
|
||||
}
|
||||
return nil
|
||||
return nil, errors.Errorf("invalid signature format")
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
package pgpsign
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/go-crypto/openpgp"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/armor"
|
||||
"github.com/ProtonMail/go-crypto/openpgp/packet"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// VerifyPolicy defines validation policy for OpenPGP signature verification.
|
||||
type VerifyPolicy struct {
|
||||
RejectExpiredKeys bool
|
||||
}
|
||||
|
||||
// ParseArmoredDetachedSignature parses a detached armored OpenPGP signature and
|
||||
// returns the first signature packet and the decoded binary signature payload.
|
||||
func ParseArmoredDetachedSignature(data []byte) (*packet.Signature, []byte, error) {
|
||||
block, err := armor.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to decode armored signature")
|
||||
}
|
||||
sigBlock, err := io.ReadAll(block.Body)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to read armored signature body")
|
||||
}
|
||||
|
||||
pr := packet.NewReader(bytes.NewReader(sigBlock))
|
||||
for {
|
||||
p, err := pr.Next()
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrap(err, "failed to read next packet")
|
||||
}
|
||||
sig, ok := p.(*packet.Signature)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return sig, sigBlock, nil
|
||||
}
|
||||
return nil, nil, errors.New("no signature packet found")
|
||||
}
|
||||
|
||||
// ReadAllArmoredKeyRings parses one or more concatenated armored OpenPGP key
|
||||
// blocks and returns a combined entity list.
|
||||
func ReadAllArmoredKeyRings(pubKeyData []byte) (openpgp.EntityList, error) {
|
||||
var ents openpgp.EntityList
|
||||
r := bytes.NewReader(pubKeyData)
|
||||
|
||||
for {
|
||||
block, err := armor.Decode(r)
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to decode armored public key")
|
||||
}
|
||||
|
||||
if block.Type != openpgp.PublicKeyType && block.Type != openpgp.PrivateKeyType {
|
||||
return nil, errors.Errorf("expected public or private key block, got: %s", block.Type)
|
||||
}
|
||||
|
||||
el, err := openpgp.ReadKeyRing(block.Body)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to read armored public key")
|
||||
}
|
||||
ents = append(ents, el...)
|
||||
}
|
||||
|
||||
if len(ents) == 0 {
|
||||
return nil, errors.New("failed to read armored public key: no armored data found")
|
||||
}
|
||||
|
||||
return ents, nil
|
||||
}
|
||||
|
||||
// VerifyArmoredDetachedSignature verifies an armored detached OpenPGP
|
||||
// signature against signedData using one or more armored public key blocks.
|
||||
func VerifyArmoredDetachedSignature(signedData io.Reader, signatureData, pubKeyData []byte, policy *VerifyPolicy) error {
|
||||
sig, sigBlock, err := ParseArmoredDetachedSignature(signatureData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ents, err := ReadAllArmoredKeyRings(pubKeyData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := checkAlgoPolicy(sig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
config := &packet.Config{}
|
||||
if policy == nil || !policy.RejectExpiredKeys {
|
||||
config.Time = func() time.Time {
|
||||
return sig.CreationTime
|
||||
}
|
||||
}
|
||||
|
||||
signer, err := openpgp.CheckDetachedSignature(
|
||||
ents,
|
||||
signedData,
|
||||
bytes.NewReader(sigBlock),
|
||||
config,
|
||||
)
|
||||
if err != nil {
|
||||
if sig.IssuerKeyId != nil {
|
||||
return errors.Wrapf(err, "signature by %X", *sig.IssuerKeyId)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
if err := checkEntityUsableForSigning(signer, now, policy); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := checkCreationTime(sig.CreationTime, now); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySignatureWithDigest verifies a parsed signature against a digest of
|
||||
// the signed payload plus OpenPGP hash suffix (payload || suffix) using the
|
||||
// provided keyring.
|
||||
func VerifySignatureWithDigest(sig *packet.Signature, keyring openpgp.EntityList, dgst digest.Digest) error {
|
||||
if sig == nil {
|
||||
return errors.New("nil signature")
|
||||
}
|
||||
|
||||
expectedAlgo, err := signatureDigestAlgorithm(sig.Hash)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dgst.Algorithm() != expectedAlgo {
|
||||
return errors.Errorf("digest algorithm mismatch: %s != %s", dgst.Algorithm(), expectedAlgo)
|
||||
}
|
||||
|
||||
sum, err := hex.DecodeString(dgst.Encoded())
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "invalid digest hex")
|
||||
}
|
||||
|
||||
h := &staticHash{sum: sum, algo: sig.Hash}
|
||||
if len(sum) != h.Size() {
|
||||
return errors.Errorf("digest size mismatch: got %d, expected %d", len(sum), h.Size())
|
||||
}
|
||||
for _, e := range keyring {
|
||||
if e.PrimaryKey != nil && e.PrimaryKey.VerifySignature(h, sig) == nil {
|
||||
return nil
|
||||
}
|
||||
for _, sub := range e.Subkeys {
|
||||
if sub.PublicKey != nil && sub.PublicKey.VerifySignature(h, sig) == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("failed to verify signature with checksum digest")
|
||||
}
|
||||
|
||||
func signatureDigestAlgorithm(h crypto.Hash) (digest.Algorithm, error) {
|
||||
switch h {
|
||||
case crypto.SHA256:
|
||||
return digest.SHA256, nil
|
||||
case crypto.SHA384:
|
||||
return digest.SHA384, nil
|
||||
case crypto.SHA512:
|
||||
return digest.SHA512, nil
|
||||
default:
|
||||
return "", errors.Errorf("unsupported signature hash algorithm %v", h)
|
||||
}
|
||||
}
|
||||
|
||||
type staticHash struct {
|
||||
sum []byte
|
||||
algo crypto.Hash
|
||||
}
|
||||
|
||||
func (s *staticHash) Write(p []byte) (n int, err error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (s *staticHash) Sum(b []byte) []byte {
|
||||
return append(b, s.sum...)
|
||||
}
|
||||
|
||||
func (s *staticHash) Reset() {}
|
||||
|
||||
func (s *staticHash) Size() int {
|
||||
switch s.algo {
|
||||
case crypto.SHA256:
|
||||
return sha256.Size
|
||||
case crypto.SHA384:
|
||||
return sha512.Size384
|
||||
case crypto.SHA512:
|
||||
return sha512.Size
|
||||
default:
|
||||
return len(s.sum)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticHash) BlockSize() int {
|
||||
switch s.algo {
|
||||
case crypto.SHA256:
|
||||
return sha256.BlockSize
|
||||
case crypto.SHA384, crypto.SHA512:
|
||||
return sha512.BlockSize
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func checkAlgoPolicy(sig *packet.Signature) error {
|
||||
switch sig.Hash {
|
||||
case crypto.SHA256, crypto.SHA384, crypto.SHA512:
|
||||
// ok
|
||||
default:
|
||||
return errors.Errorf("rejecting weak/unknown hash: %v", sig.Hash)
|
||||
}
|
||||
|
||||
switch sig.PubKeyAlgo {
|
||||
case packet.PubKeyAlgoEdDSA, packet.PubKeyAlgoECDSA, packet.PubKeyAlgoRSA, packet.PubKeyAlgoRSASignOnly:
|
||||
default:
|
||||
return errors.Errorf("rejecting unsupported pubkey algorithm: %v", sig.PubKeyAlgo)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEntityUsableForSigning(e *openpgp.Entity, now time.Time, policy *VerifyPolicy) error {
|
||||
if e == nil || e.PrimaryKey == nil {
|
||||
return errors.New("nil entity or key")
|
||||
}
|
||||
|
||||
if policy != nil && policy.RejectExpiredKeys {
|
||||
if id := e.PrimaryIdentity(); id != nil && id.SelfSignature != nil {
|
||||
if exp := id.SelfSignature.KeyLifetimeSecs; exp != nil && *exp > 0 {
|
||||
expiry := e.PrimaryKey.CreationTime.Add(time.Duration(*exp) * time.Second)
|
||||
if now.After(expiry) {
|
||||
return errors.Errorf("key expired at %v", expiry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := checkEntityRevocation(e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if rsaPub, ok := e.PrimaryKey.PublicKey.(*rsa.PublicKey); ok {
|
||||
if rsaPub.N.BitLen() < 2048 {
|
||||
return errors.Errorf("RSA key too short: %d bits", rsaPub.N.BitLen())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkEntityRevocation(e *openpgp.Entity) error {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
for _, r := range e.Revocations {
|
||||
if r == nil || r.SigType != packet.SigTypeKeyRevocation {
|
||||
continue
|
||||
}
|
||||
if err := e.PrimaryKey.VerifyRevocationSignature(r); err != nil {
|
||||
continue
|
||||
}
|
||||
if r.RevocationReasonText != "" {
|
||||
return errors.Errorf("key revoked: %s", r.RevocationReasonText)
|
||||
}
|
||||
return errors.New("key revoked")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkCreationTime(sigTime, now time.Time) error {
|
||||
if sigTime.After(now.Add(5 * time.Minute)) {
|
||||
return errors.Errorf("signature creation time is in the future: %v", sigTime)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user