summaryrefslogtreecommitdiff
path: root/vendor/github.com/containers/image/v5/signature/simple.go
blob: 94a84659302bfca823932212acc15a8252a5e06c (plain)
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
// Note: Consider the API unstable until the code supports at least three different image formats or transports.

// NOTE: Keep this in sync with docs/atomic-signature.md and docs/atomic-signature-embedded.json!

package signature

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/containers/image/v5/signature/internal"
	"github.com/containers/image/v5/version"
	digest "github.com/opencontainers/go-digest"
)

const (
	signatureType = "atomic container signature"
)

// InvalidSignatureError is returned when parsing an invalid signature.
type InvalidSignatureError = internal.InvalidSignatureError

// Signature is a parsed content of a signature.
// The only way to get this structure from a blob should be as a return value from a successful call to verifyAndExtractSignature below.
type Signature struct {
	DockerManifestDigest digest.Digest
	DockerReference      string // FIXME: more precise type?
}

// untrustedSignature is a parsed content of a signature.
type untrustedSignature struct {
	untrustedDockerManifestDigest digest.Digest
	untrustedDockerReference      string // FIXME: more precise type?
	untrustedCreatorID            *string
	// This is intentionally an int64; the native JSON float64 type would allow to represent _some_ sub-second precision,
	// but not nearly enough (with current timestamp values, a single unit in the last place is on the order of hundreds of nanoseconds).
	// So, this is explicitly an int64, and we reject fractional values. If we did need more precise timestamps eventually,
	// we would add another field, UntrustedTimestampNS int64.
	untrustedTimestamp *int64
}

// UntrustedSignatureInformation is information available in an untrusted signature.
// This may be useful when debugging signature verification failures,
// or when managing a set of signatures on a single image.
//
// WARNING: Do not use the contents of this for ANY security decisions,
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
// There is NO REASON to expect the values to be correct, or not intentionally misleading
// (including things like “✅ Verified by $authority”)
type UntrustedSignatureInformation struct {
	UntrustedDockerManifestDigest digest.Digest
	UntrustedDockerReference      string // FIXME: more precise type?
	UntrustedCreatorID            *string
	UntrustedTimestamp            *time.Time
	UntrustedShortKeyIdentifier   string
}

// newUntrustedSignature returns an untrustedSignature object with
// the specified primary contents and appropriate metadata.
func newUntrustedSignature(dockerManifestDigest digest.Digest, dockerReference string) untrustedSignature {
	// Use intermediate variables for these values so that we can take their addresses.
	// Golang guarantees that they will have a new address on every execution.
	creatorID := "atomic " + version.Version
	timestamp := time.Now().Unix()
	return untrustedSignature{
		untrustedDockerManifestDigest: dockerManifestDigest,
		untrustedDockerReference:      dockerReference,
		untrustedCreatorID:            &creatorID,
		untrustedTimestamp:            &timestamp,
	}
}

// A compile-time check that untrustedSignature  and *untrustedSignature implements json.Marshaler
var _ json.Marshaler = untrustedSignature{}
var _ json.Marshaler = (*untrustedSignature)(nil)

// MarshalJSON implements the json.Marshaler interface.
func (s untrustedSignature) MarshalJSON() ([]byte, error) {
	if s.untrustedDockerManifestDigest == "" || s.untrustedDockerReference == "" {
		return nil, errors.New("Unexpected empty signature content")
	}
	critical := map[string]any{
		"type":     signatureType,
		"image":    map[string]string{"docker-manifest-digest": s.untrustedDockerManifestDigest.String()},
		"identity": map[string]string{"docker-reference": s.untrustedDockerReference},
	}
	optional := map[string]any{}
	if s.untrustedCreatorID != nil {
		optional["creator"] = *s.untrustedCreatorID
	}
	if s.untrustedTimestamp != nil {
		optional["timestamp"] = *s.untrustedTimestamp
	}
	signature := map[string]any{
		"critical": critical,
		"optional": optional,
	}
	return json.Marshal(signature)
}

// Compile-time check that untrustedSignature implements json.Unmarshaler
var _ json.Unmarshaler = (*untrustedSignature)(nil)

// UnmarshalJSON implements the json.Unmarshaler interface
func (s *untrustedSignature) UnmarshalJSON(data []byte) error {
	return internal.JSONFormatToInvalidSignatureError(s.strictUnmarshalJSON(data))
}

// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal.JSONFormatError error type.
// Splitting it into a separate function allows us to do the internal.JSONFormatError → InvalidSignatureError in a single place, the caller.
func (s *untrustedSignature) strictUnmarshalJSON(data []byte) error {
	var critical, optional json.RawMessage
	if err := internal.ParanoidUnmarshalJSONObjectExactFields(data, map[string]any{
		"critical": &critical,
		"optional": &optional,
	}); err != nil {
		return err
	}

	var creatorID string
	var timestamp float64
	var gotCreatorID, gotTimestamp = false, false
	if err := internal.ParanoidUnmarshalJSONObject(optional, func(key string) any {
		switch key {
		case "creator":
			gotCreatorID = true
			return &creatorID
		case "timestamp":
			gotTimestamp = true
			return &timestamp
		default:
			var ignore any
			return &ignore
		}
	}); err != nil {
		return err
	}
	if gotCreatorID {
		s.untrustedCreatorID = &creatorID
	}
	if gotTimestamp {
		intTimestamp := int64(timestamp)
		if float64(intTimestamp) != timestamp {
			return internal.NewInvalidSignatureError("Field optional.timestamp is not an integer")
		}
		s.untrustedTimestamp = &intTimestamp
	}

	var t string
	var image, identity json.RawMessage
	if err := internal.ParanoidUnmarshalJSONObjectExactFields(critical, map[string]any{
		"type":     &t,
		"image":    &image,
		"identity": &identity,
	}); err != nil {
		return err
	}
	if t != signatureType {
		return internal.NewInvalidSignatureError(fmt.Sprintf("Unrecognized signature type %s", t))
	}

	var digestString string
	if err := internal.ParanoidUnmarshalJSONObjectExactFields(image, map[string]any{
		"docker-manifest-digest": &digestString,
	}); err != nil {
		return err
	}
	digestValue, err := digest.Parse(digestString)
	if err != nil {
		return internal.NewInvalidSignatureError(fmt.Sprintf(`invalid docker-manifest-digest value %q: %v`, digestString, err))
	}
	s.untrustedDockerManifestDigest = digestValue

	return internal.ParanoidUnmarshalJSONObjectExactFields(identity, map[string]any{
		"docker-reference": &s.untrustedDockerReference,
	})
}

// Sign formats the signature and returns a blob signed using mech and keyIdentity
// (If it seems surprising that this is a method on untrustedSignature, note that there
// isn’t a good reason to think that a key used by the user is trusted by any component
// of the system just because it is a private key — actually the presence of a private key
// on the system increases the likelihood of an a successful attack on that private key
// on that particular system.)
func (s untrustedSignature) sign(mech SigningMechanism, keyIdentity string, passphrase string) ([]byte, error) {
	json, err := json.Marshal(s)
	if err != nil {
		return nil, err
	}

	if newMech, ok := mech.(signingMechanismWithPassphrase); ok {
		return newMech.SignWithPassphrase(json, keyIdentity, passphrase)
	}

	if passphrase != "" {
		return nil, errors.New("signing mechanism does not support passphrases")
	}

	return mech.Sign(json, keyIdentity)
}

// signatureAcceptanceRules specifies how to decide whether an untrusted signature is acceptable.
// We centralize the actual parsing and data extraction in verifyAndExtractSignature; this supplies
// the policy.  We use an object instead of supplying func parameters to verifyAndExtractSignature
// because the functions have the same or similar types, so there is a risk of exchanging the functions;
// named members of this struct are more explicit.
type signatureAcceptanceRules struct {
	validateKeyIdentity                func(string) error
	validateSignedDockerReference      func(string) error
	validateSignedDockerManifestDigest func(digest.Digest) error
}

// verifyAndExtractSignature verifies that unverifiedSignature has been signed, and that its principal components
// match expected values, both as specified by rules, and returns it
func verifyAndExtractSignature(mech SigningMechanism, unverifiedSignature []byte, rules signatureAcceptanceRules) (*Signature, error) {
	signed, keyIdentity, err := mech.Verify(unverifiedSignature)
	if err != nil {
		return nil, err
	}
	if err := rules.validateKeyIdentity(keyIdentity); err != nil {
		return nil, err
	}

	var unmatchedSignature untrustedSignature
	if err := json.Unmarshal(signed, &unmatchedSignature); err != nil {
		return nil, internal.NewInvalidSignatureError(err.Error())
	}
	if err := rules.validateSignedDockerManifestDigest(unmatchedSignature.untrustedDockerManifestDigest); err != nil {
		return nil, err
	}
	if err := rules.validateSignedDockerReference(unmatchedSignature.untrustedDockerReference); err != nil {
		return nil, err
	}
	// signatureAcceptanceRules have accepted this value.
	return &Signature{
		DockerManifestDigest: unmatchedSignature.untrustedDockerManifestDigest,
		DockerReference:      unmatchedSignature.untrustedDockerReference,
	}, nil
}

// GetUntrustedSignatureInformationWithoutVerifying extracts information available in an untrusted signature,
// WITHOUT doing any cryptographic verification.
// This may be useful when debugging signature verification failures,
// or when managing a set of signatures on a single image.
//
// WARNING: Do not use the contents of this for ANY security decisions,
// and be VERY CAREFUL about showing this information to humans in any way which suggest that these values “are probably” reliable.
// There is NO REASON to expect the values to be correct, or not intentionally misleading
// (including things like “✅ Verified by $authority”)
func GetUntrustedSignatureInformationWithoutVerifying(untrustedSignatureBytes []byte) (*UntrustedSignatureInformation, error) {
	// NOTE: This should eventually do format autodetection.
	mech, _, err := NewEphemeralGPGSigningMechanism([]byte{})
	if err != nil {
		return nil, err
	}
	defer mech.Close()

	untrustedContents, shortKeyIdentifier, err := mech.UntrustedSignatureContents(untrustedSignatureBytes)
	if err != nil {
		return nil, err
	}
	var untrustedDecodedContents untrustedSignature
	if err := json.Unmarshal(untrustedContents, &untrustedDecodedContents); err != nil {
		return nil, internal.NewInvalidSignatureError(err.Error())
	}

	var timestamp *time.Time // = nil
	if untrustedDecodedContents.untrustedTimestamp != nil {
		ts := time.Unix(*untrustedDecodedContents.untrustedTimestamp, 0)
		timestamp = &ts
	}
	return &UntrustedSignatureInformation{
		UntrustedDockerManifestDigest: untrustedDecodedContents.untrustedDockerManifestDigest,
		UntrustedDockerReference:      untrustedDecodedContents.untrustedDockerReference,
		UntrustedCreatorID:            untrustedDecodedContents.untrustedCreatorID,
		UntrustedTimestamp:            timestamp,
		UntrustedShortKeyIdentifier:   shortKeyIdentifier,
	}, nil
}