File: sign.go

package info (click to toggle)
golang-github-sylabs-sif 2.21.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 5,200 kB
  • sloc: makefile: 6
file content (431 lines) | stat: -rw-r--r-- 12,245 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
// Copyright (c) 2020-2024, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the LICENSE.md file
// distributed with the sources of this project regarding your rights to use or distribute this
// software.

package integrity

import (
	"bytes"
	"context"
	"crypto"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"slices"
	"time"

	"github.com/ProtonMail/go-crypto/openpgp"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/sigstore/sigstore/pkg/signature"
	"github.com/sylabs/sif/v2/pkg/sif"
)

var (
	errNoObjectsSpecified = errors.New("no objects specified")
	errUnexpectedGroupID  = errors.New("unexpected group ID")
	errNilFileImage       = errors.New("nil file image")
)

// ErrNoKeyMaterial is the error returned when no key material was provided.
var ErrNoKeyMaterial = errors.New("key material not provided")

type encoder interface {
	// signMessage signs the message from r, and writes the result to w. On success, the signature
	// hash function is returned.
	signMessage(ctx context.Context, w io.Writer, r io.Reader) (ht crypto.Hash, err error)
}

type groupSigner struct {
	en     encoder          // Message encoder.
	f      *sif.FileImage   // SIF image to sign.
	id     uint32           // Group ID.
	ods    []sif.Descriptor // Descriptors of object(s) to sign.
	mdHash crypto.Hash      // Hash type for metadata.
	fp     []byte           // Fingerprint of signing entity.
}

// groupSignerOpt are used to configure gs.
type groupSignerOpt func(gs *groupSigner) error

// optSignGroupObjects specifies the signature include objects with the specified ids.
func optSignGroupObjects(ids ...uint32) groupSignerOpt {
	return func(gs *groupSigner) error {
		if len(ids) == 0 {
			return errNoObjectsSpecified
		}

		for _, id := range ids {
			od, err := gs.f.GetDescriptor(sif.WithID(id))
			if err != nil {
				return err
			}

			if err := gs.addObject(od); err != nil {
				return err
			}
		}

		return nil
	}
}

// optSignGroupMetadataHash sets h as the metadata hash function.
func optSignGroupMetadataHash(h crypto.Hash) groupSignerOpt {
	return func(gs *groupSigner) error {
		gs.mdHash = h
		return nil
	}
}

// optSignGroupFingerprint sets fp as the fingerprint of the signing entity.
func optSignGroupFingerprint(fp []byte) groupSignerOpt {
	return func(gs *groupSigner) error {
		gs.fp = fp
		return nil
	}
}

// newGroupSigner returns a new groupSigner to add a digital signature using en for the specified
// group to f, according to opts.
//
// By default, all data objects in the group will be signed. To override this behavior, use
// optSignGroupObjects(). To override the default metadata hash algorithm, use
// optSignGroupMetadataHash().
//
// By default, the fingerprint of the signing entity is not set. To override this behavior, use
// optSignGroupFingerprint.
func newGroupSigner(en encoder, f *sif.FileImage, groupID uint32, opts ...groupSignerOpt) (*groupSigner, error) {
	if groupID == 0 {
		return nil, sif.ErrInvalidGroupID
	}

	gs := groupSigner{
		en:     en,
		f:      f,
		id:     groupID,
		mdHash: crypto.SHA256,
	}

	// Apply options.
	for _, opt := range opts {
		if err := opt(&gs); err != nil {
			return nil, err
		}
	}

	// If no object descriptors specified, select all in group.
	if len(gs.ods) == 0 {
		ods, err := getGroupObjects(f, groupID)
		if err != nil {
			return nil, err
		}

		for _, od := range ods {
			if err := gs.addObject(od); err != nil {
				return nil, err
			}
		}
	}

	return &gs, nil
}

// addObject adds od to the list of object descriptors to be signed.
func (gs *groupSigner) addObject(od sif.Descriptor) error {
	if groupID := od.GroupID(); groupID != gs.id {
		return fmt.Errorf("%w (%v)", errUnexpectedGroupID, groupID)
	}

	// Insert into sorted descriptor list, if not already present.
	gs.ods = insertSortedFunc(gs.ods, od, func(a, b sif.Descriptor) int { return int(a.ID()) - int(b.ID()) })

	return nil
}

// sign creates a digital signature as specified by gs.
func (gs *groupSigner) sign(ctx context.Context) (sif.DescriptorInput, error) {
	// Get minimum object ID in group. Object IDs in the image metadata will be relative to this.
	minID, err := getGroupMinObjectID(gs.f, gs.id)
	if err != nil {
		return sif.DescriptorInput{}, err
	}

	// Get metadata for the image.
	md, err := getImageMetadata(gs.f, minID, gs.ods, gs.mdHash)
	if err != nil {
		return sif.DescriptorInput{}, fmt.Errorf("failed to get image metadata: %w", err)
	}

	// Encode image metadata.
	enc, err := json.Marshal(md)
	if err != nil {
		return sif.DescriptorInput{}, fmt.Errorf("failed to encode image metadata: %w", err)
	}

	// Sign image metadata.
	b := bytes.Buffer{}
	ht, err := gs.en.signMessage(ctx, &b, bytes.NewReader(enc))
	if err != nil {
		return sif.DescriptorInput{}, fmt.Errorf("failed to sign message: %w", err)
	}

	// Prepare SIF data object descriptor.
	return sif.NewDescriptorInput(sif.DataSignature, &b,
		sif.OptNoGroup(),
		sif.OptLinkedGroupID(gs.id),
		sif.OptSignatureMetadata(ht, gs.fp),
	)
}

type signOpts struct {
	ss                      []signature.Signer
	e                       *openpgp.Entity
	groupIDs                []uint32
	objectIDs               [][]uint32
	timeFunc                func() time.Time
	deterministic           bool
	ctx                     context.Context //nolint:containedctx
	withoutPGPSignatureSalt bool
}

// SignerOpt are used to configure so.
type SignerOpt func(so *signOpts) error

// OptSignWithSigner specifies signer(s) to use to generate signature(s).
func OptSignWithSigner(ss ...signature.Signer) SignerOpt {
	return func(so *signOpts) error {
		so.ss = append(so.ss, ss...)
		return nil
	}
}

// OptSignWithEntity specifies e as the entity to use to generate signature(s).
func OptSignWithEntity(e *openpgp.Entity) SignerOpt {
	return func(so *signOpts) error {
		so.e = e
		return nil
	}
}

// OptSignGroup specifies that a signature be applied to cover all objects in the group with the
// specified groupID. This may be called multiple times to add multiple group signatures.
func OptSignGroup(groupID uint32) SignerOpt {
	return func(so *signOpts) error {
		so.groupIDs = append(so.groupIDs, groupID)
		return nil
	}
}

// OptSignObjects specifies that one or more signature(s) be applied to cover objects with the
// specified ids. One signature will be applied for each group ID associated with the object(s).
// This may be called multiple times to add multiple signatures.
func OptSignObjects(ids ...uint32) SignerOpt {
	return func(so *signOpts) error {
		if len(ids) == 0 {
			return errNoObjectsSpecified
		}

		so.objectIDs = append(so.objectIDs, ids)
		return nil
	}
}

// OptSignWithTime specifies fn as the func to obtain signature timestamp(s). Unless
// OptSignDeterministic is supplied, fn is also used to set SIF timestamps.
func OptSignWithTime(fn func() time.Time) SignerOpt {
	return func(so *signOpts) error {
		so.timeFunc = fn
		return nil
	}
}

// OptSignDeterministic sets SIF header/descriptor fields to values that support deterministic
// modification of images. This does not affect the signature timestamps; to specify deterministic
// signature timestamps, use OptSignWithTime.
func OptSignDeterministic() SignerOpt {
	return func(so *signOpts) error {
		so.deterministic = true
		return nil
	}
}

// OptSignWithContext specifies that the given context should be used in RPC to external services.
func OptSignWithContext(ctx context.Context) SignerOpt {
	return func(so *signOpts) error {
		so.ctx = ctx
		return nil
	}
}

// OptSignWithoutPGPSignatureSalt disables the addition of a salt notation for v4 and v5 PGP keys.
// While this increases determinism, it should be used with caution as the salt notation increases
// protection for certain kinds of attacks.
func OptSignWithoutPGPSignatureSalt() SignerOpt {
	return func(so *signOpts) error {
		so.withoutPGPSignatureSalt = true
		return nil
	}
}

// withGroupedObjects splits the objects represented by ids into object groups, and calls fn once
// per object group.
func withGroupedObjects(f *sif.FileImage, ids []uint32, fn func(uint32, []uint32) error) error {
	var groupIDs []uint32
	groupObjectIDs := make(map[uint32][]uint32)

	for _, id := range ids {
		od, err := f.GetDescriptor(sif.WithID(id))
		if err != nil {
			return err
		}

		// Note the group ID if it hasn't been seen before, and append the object ID to the
		// appropriate group in the map.
		groupID := od.GroupID()
		if _, ok := groupObjectIDs[groupID]; !ok {
			groupIDs = append(groupIDs, groupID)
		}
		groupObjectIDs[groupID] = append(groupObjectIDs[groupID], id)
	}

	slices.Sort(groupIDs)

	for _, groupID := range groupIDs {
		if err := fn(groupID, groupObjectIDs[groupID]); err != nil {
			return err
		}
	}

	return nil
}

// Signer describes a SIF image signer.
type Signer struct {
	f       *sif.FileImage
	opts    signOpts
	signers []*groupSigner
}

// NewSigner returns a Signer to add digital signature(s) to f, according to opts. Key material
// must be provided, or an error wrapping ErrNoKeyMaterial is returned.
//
// To provide key material, consider using OptSignWithSigner or OptSignWithEntity.
//
// By default, one digital signature is added per object group in f. To override this behavior,
// consider using OptSignGroup and/or OptSignObjects.
//
// By default, signature timestamps are set to the current time. To override this behavior,
// consider using OptSignWithTime.
//
// By default, header and descriptor timestamps are set to the current time for non-deterministic
// images, and unset otherwise. To override this behavior, consider using OptSignWithTime or
// OptSignDeterministic.
func NewSigner(f *sif.FileImage, opts ...SignerOpt) (*Signer, error) {
	if f == nil {
		return nil, fmt.Errorf("integrity: %w", errNilFileImage)
	}

	so := signOpts{
		ctx: context.Background(),
	}

	// Apply options.
	for _, opt := range opts {
		if err := opt(&so); err != nil {
			return nil, fmt.Errorf("integrity: %w", err)
		}
	}

	s := Signer{
		f:    f,
		opts: so,
	}

	var commonOpts []groupSignerOpt

	// Get message encoder.
	var en encoder
	switch {
	case so.ss != nil:
		en = newDSSEEncoder(so.ss)
	case so.e != nil:
		en = newClearsignEncoder(so.e, &packet.Config{
			Time:                                  so.timeFunc,
			NonDeterministicSignaturesViaNotation: packet.BoolPointer(!so.withoutPGPSignatureSalt),
		})
		commonOpts = append(commonOpts, optSignGroupFingerprint(so.e.PrimaryKey.Fingerprint))
	default:
		return nil, fmt.Errorf("integrity: %w", ErrNoKeyMaterial)
	}

	// Add signer for each groupID.
	for _, groupID := range so.groupIDs {
		gs, err := newGroupSigner(en, f, groupID, commonOpts...)
		if err != nil {
			return nil, fmt.Errorf("integrity: %w", err)
		}
		s.signers = append(s.signers, gs)
	}

	// Add signer(s) for each list of object IDs.
	for _, ids := range so.objectIDs {
		err := withGroupedObjects(f, ids, func(groupID uint32, ids []uint32) error {
			opts := commonOpts
			opts = append(opts, optSignGroupObjects(ids...))

			gs, err := newGroupSigner(en, f, groupID, opts...)
			if err != nil {
				return err
			}
			s.signers = append(s.signers, gs)

			return nil
		})
		if err != nil {
			return nil, fmt.Errorf("integrity: %w", err)
		}
	}

	// If no signers specified, add one per object group.
	if len(s.signers) == 0 {
		ids, err := getGroupIDs(f)
		if err != nil {
			return nil, fmt.Errorf("integrity: %w", err)
		}

		for _, id := range ids {
			gs, err := newGroupSigner(en, f, id, commonOpts...)
			if err != nil {
				return nil, fmt.Errorf("integrity: %w", err)
			}
			s.signers = append(s.signers, gs)
		}
	}

	return &s, nil
}

// Sign adds digital signatures as specified by s.
func (s *Signer) Sign() error {
	for _, gs := range s.signers {
		di, err := gs.sign(s.opts.ctx)
		if err != nil {
			return fmt.Errorf("integrity: %w", err)
		}

		var opts []sif.AddOpt
		if s.opts.deterministic {
			opts = append(opts, sif.OptAddDeterministic())
		} else if s.opts.timeFunc != nil {
			opts = append(opts, sif.OptAddWithTime(s.opts.timeFunc()))
		}

		if err := s.f.AddObject(di, opts...); err != nil {
			return fmt.Errorf("integrity: failed to add object: %w", err)
		}
	}

	return nil
}