/
cosign_payload.go
160 lines (146 loc) · 5.74 KB
/
cosign_payload.go
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
package internal
import (
"encoding/json"
"errors"
"fmt"
"time"
"github.com/containers/image/v5/version"
digest "github.com/opencontainers/go-digest"
)
const (
cosignSignatureType = "cosign container image signature"
)
// UntrustedCosignPayload is a parsed content of a Cosign signature payload (not the full signature)
type UntrustedCosignPayload 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
}
// NewUntrustedCosignPayload returns an untrustedCosignPayload object with
// the specified primary contents and appropriate metadata.
func NewUntrustedCosignPayload(dockerManifestDigest digest.Digest, dockerReference string) UntrustedCosignPayload {
// FIXME: Move this comment to the caller.
// AARGH. sigstore/cosign mostly ignores dockerReference for actual policy decisions.
// That’s bad enough, BUT they also:
// - Record the repo (but NOT THE TAG) in the value; without the tag we can’t detect version rollbacks.
// - parse dockerReference @ dockerManifestDigest and expect that to be valid.
// - It seems (FIXME: TEST THIS) that putting a repo:tag in would pass the current implementation.
// And signing digest references is _possible_ but probably rare (because signing typically happens on push, when
// the digest reference is not known in advance).
// SO: We put the full value in, which is not interoperable for signed digest references right now,
// and TODO: Talk sigstore/cosign to relax that.
//
// 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 := "containers/image " + version.Version
timestamp := time.Now().Unix()
return UntrustedCosignPayload{
UntrustedDockerManifestDigest: dockerManifestDigest,
UntrustedDockerReference: dockerReference,
UntrustedCreatorID: &creatorID,
UntrustedTimestamp: ×tamp,
}
}
// Compile-time check that untrustedCosignPayload implements json.Marshaler
var _ json.Marshaler = (*UntrustedCosignPayload)(nil)
// MarshalJSON implements the json.Marshaler interface.
func (s UntrustedCosignPayload) MarshalJSON() ([]byte, error) {
if s.UntrustedDockerManifestDigest == "" || s.UntrustedDockerReference == "" {
return nil, errors.New("Unexpected empty signature content")
}
critical := map[string]interface{}{
"type": cosignSignatureType,
"image": map[string]string{"docker-manifest-digest": s.UntrustedDockerManifestDigest.String()},
"identity": map[string]string{"docker-reference": s.UntrustedDockerReference},
}
optional := map[string]interface{}{}
if s.UntrustedCreatorID != nil {
optional["creator"] = *s.UntrustedCreatorID
}
if s.UntrustedTimestamp != nil {
optional["timestamp"] = *s.UntrustedTimestamp
}
signature := map[string]interface{}{
"critical": critical,
"optional": optional,
}
return json.Marshal(signature)
}
// Compile-time check that untrustedCosignPayload implements json.Unmarshaler
var _ json.Unmarshaler = (*UntrustedCosignPayload)(nil)
// UnmarshalJSON implements the json.Unmarshaler interface
func (s *UntrustedCosignPayload) UnmarshalJSON(data []byte) error {
err := s.strictUnmarshalJSON(data)
if err != nil {
if formatErr, ok := err.(JSONFormatError); ok {
err = NewInvalidSignatureError(formatErr.Error())
}
}
return err
}
// strictUnmarshalJSON is UnmarshalJSON, except that it may return the internal JSONFormatError error type.
// Splitting it into a separate function allows us to do the JSONFormatError → InvalidSignatureError in a single place, the caller.
func (s *UntrustedCosignPayload) strictUnmarshalJSON(data []byte) error {
var critical, optional json.RawMessage
if err := ParanoidUnmarshalJSONObjectExactFields(data, map[string]interface{}{
"critical": &critical,
"optional": &optional,
}); err != nil {
return err
}
var creatorID string
var timestamp float64
var gotCreatorID, gotTimestamp = false, false
if err := ParanoidUnmarshalJSONObject(optional, func(key string) interface{} {
switch key {
case "creator":
gotCreatorID = true
return &creatorID
case "timestamp":
gotTimestamp = true
return ×tamp
default:
var ignore interface{}
return &ignore
}
}); err != nil {
return err
}
if gotCreatorID {
s.UntrustedCreatorID = &creatorID
}
if gotTimestamp {
intTimestamp := int64(timestamp)
if float64(intTimestamp) != timestamp {
return NewInvalidSignatureError("Field optional.timestamp is not is not an integer")
}
s.UntrustedTimestamp = &intTimestamp
}
var t string
var image, identity json.RawMessage
if err := ParanoidUnmarshalJSONObjectExactFields(critical, map[string]interface{}{
"type": &t,
"image": &image,
"identity": &identity,
}); err != nil {
return err
}
if t != cosignSignatureType {
return NewInvalidSignatureError(fmt.Sprintf("Unrecognized signature type %s", t))
}
var digestString string
if err := ParanoidUnmarshalJSONObjectExactFields(image, map[string]interface{}{
"docker-manifest-digest": &digestString,
}); err != nil {
return err
}
s.UntrustedDockerManifestDigest = digest.Digest(digestString)
return ParanoidUnmarshalJSONObjectExactFields(identity, map[string]interface{}{
"docker-reference": &s.UntrustedDockerReference,
})
}