Skip to content

Commit

Permalink
Add support for Fulcio and Rekor to sigstoreSigned
Browse files Browse the repository at this point in the history
Signed-off-by: Miloslav Trmač <mitr@redhat.com>
  • Loading branch information
mtrmac committed Jan 21, 2023
1 parent 5158076 commit 23774f5
Show file tree
Hide file tree
Showing 12 changed files with 1,200 additions and 93 deletions.
65 changes: 61 additions & 4 deletions docs/containers-policy.json.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,12 +247,37 @@ This requirement requires an image to be signed using a sigstore signature with
```js
{
"type": "sigstoreSigned",
"keyPath": "/path/to/local/keyring/file",
"keyData": "base64-encoded-keyring-data",
"keyPath": "/path/to/local/public/key/file",
"keyData": "base64-encoded-public-key-data",
"fulcio": {
"caPath": "/path/to/local/CA/file",
"caData": "base64-encoded-CA-data",
"oidcIssuer": "https://expected.OIDC.issuer/",
"subjectEmail", "expected-signing-user@example.com",
},
"rekorPublicKeyPath": "/path/to/local/public/key/file",
"rekorPublicKeyData": "base64-encoded-public-key-data",
"signedIdentity": identity_requirement
}
```
Exactly one of `keyPath` and `keyData` must be present, containing a sigstore public key. Only signatures made by this key is accepted.
Exactly one of `keyPath`, `keyData` and `fulcio` must be present.

If `keyPath` or `keyData` is present, it contains a sigstore public key.
Only signatures made by this key are accepted.

If `fulcio` is present, the signature must be based on a Fulcio-issued certificate.
One of `caPath` and `caData` must be specified, containing the public key of the Fulcio instance.
Both `oidcIssuer` and `subjectEmail` are mandatory,
exactly specifying the expected identity provider,
and the identity of the user obtaining the Fulcio certificate.

At most one of `rekorPublicKeyPath` and `rekorPublicKeyData` can be present;
it is mandatory if `fulcio` is specified.
If a Rekor public key is specified,
the signature must have been uploaded to a Rekor server
and the signature must contain an (offline-verifiable) “signed entry timestamp”
proving the existence of the Rekor log record,
signed by the provided public key.

The `signedIdentity` field has the same semantics as in the `signedBy` requirement described above.
Note that `cosign`-created signatures only contain a repository, so only `matchRepository` and `exactRepository` can be used to accept them (and that does not protect against substitution of a signed image with an unexpected tag).
Expand Down Expand Up @@ -288,15 +313,47 @@ selectively allow individual transports and scopes as desired.
"keyPath": "/path/to/sigstore-pubkey.pub"
}
],
/* A sigstore-signed repository using the community Fulcio+Rekor servers.
The community servers’ public keys can be obtained from
https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */
"hostname:5000/myns/sigstore-signed-fulcio-rekor": [
{
"type": "sigstoreSigned",
"fulcio": {
"caPath": "/path/to/fulcio_v1.crt.pem",
"oidcIssuer": "https://github.com/login/oauth",
"subjectEmail": "test-user@example.com"
},
"rekorPublicKeyPath": "/path/to/rekor.pub",
}
],
/* A sigstore-signed repository, accepts signatures by /usr/bin/cosign */
"hostname:5000/myns/sigstore-signed-allows-malicious-tag-substitution": [
{
"type": "sigstoreSigned",
"keyPath": "/path/to/sigstore-pubkey.pub",
"signedIdentity": {"type": "matchRepository"}
}
],
/* A sigstore-signed repository using the community Fulcio+Rekor servers,
accepts signatures by /usr/bin/cosign.
The community servers’ public keys can be obtained from
https://github.com/sigstore/sigstore/tree/main/pkg/tuf/repository/targets . */
"hostname:5000/myns/sigstore-signed-fulcio-rekor- allows-malicious-tag-substitution": [
{
"type": "sigstoreSigned",
"fulcio": {
"caPath": "/path/to/fulcio_v1.crt.pem",
"oidcIssuer": "https://github.com/login/oauth",
"subjectEmail": "test-user@example.com"
},
"rekorPublicKeyPath": "/path/to/rekor.pub",
"signedIdentity": { "type": "matchRepository" }
}
]
/* Other docker: images use the global default policy and are rejected */
/* Other docker: images use the global default policy and are rejected */
},
"dir": {
"": [{"type": "insecureAcceptAnything"}] /* Allow any images originating in local directories */
Expand Down
4 changes: 4 additions & 0 deletions signature/fixtures/cosign2.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEwOkOF9xpfG8ghueIhnZ66ooujwt1
+ReV3HupgKnGFYnEh3Hh1YTg5L6kN1Yakkt5WltRoav8/R3hpCtUO3Rldw==
-----END PUBLIC KEY-----
16 changes: 16 additions & 0 deletions signature/fixtures/dir-img-cosign-fulcio-rekor-valid/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1508,
"digest": "sha256:3b0f78b718417dfa432fd8da26d0e3ac4ca3566d0825b0d2b056ccc4dd29e644"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2568440,
"digest": "sha256:1df32bae7504a32024616c66017cd5df04dd98eaf150f8df45fffef2547a3c54"
}
]
}
Binary file not shown.
16 changes: 16 additions & 0 deletions signature/fixtures/dir-img-cosign-key-rekor-valid/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1508,
"digest": "sha256:3b0f78b718417dfa432fd8da26d0e3ac4ca3566d0825b0d2b056ccc4dd29e644"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2568440,
"digest": "sha256:1df32bae7504a32024616c66017cd5df04dd98eaf150f8df45fffef2547a3c54"
}
]
}
Binary file not shown.
14 changes: 14 additions & 0 deletions signature/fixtures/some-rsa-key.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA1bofelEsr9ZYEDlNZN+Z
K4LbdXi1grcUYGk9S7UsaZV4ovq++j5yYAiM2mWBxcmD65pokyanLEz/0ya9zSez
C1dY337ecoRj7OgLzrMJarKJu3xDYe/jCylQZCke7bobsIahi00i6sU0Feh94ULt
aSiBaOEZzYvbE6QBsfQWwH2EC/Ch0Dsy6zpZQxnsEGJkpr/ed9UYwUlgaGAouL4N
A5flQvDNWyKNMBsosrTHE+yRYiouAytszJ1WzMQrR8n3ngsMaJtM5FZClyT1FbB5
YnvoFcTUqVQqMFCK6DBQvMp5mOBHAdWaHXTAP9cKFK73CdQwHigP3ayXRyq/Npjd
tSpk1AW12Ctkiu9jIX1upGpwJCz0nevsXm5CL2jdxbzDPxIHV4w4kHxt6CcCWJ3V
DSqTIV8xGMyRahUsmhnRXeRSF0UaQdzI9ZPKWW20bYjZ3qJawO3YhJZUwxZKiAm6
Px2nd4lVpJXZLZ/DJ7eXJ0rWiwC2Y3C+1FlXdWtbocersg6a7oW/VNe+unVznHWm
N0GSp1IobRsvP6t5ITIJiQguROl8PVNS7Wu4vzDndy1cH4NBCyrQopQLJ29U51S4
2tqYlSlJReWjvSidCSZDa9/YXU9LZqVWmkSKwMcCKCRACIYOLxFZJjsxj5hOIOkT
4EeUfiK04GAV1QKVloZ9b2sCAwEAAQ==
-----END PUBLIC KEY-----
205 changes: 200 additions & 5 deletions signature/policy_config_sigstore.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,39 @@ func PRSigstoreSignedWithKeyData(keyData []byte) PRSigstoreSignedOption {
}
}

// PRSigstoreSignedWithFulcio specifies a value for the "fulcio" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithFulcio(fulcio PRSigstoreSignedFulcio) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.Fulcio != nil {
return errors.New(`"fulcio" already specified`)
}
pr.Fulcio = fulcio
return nil
}
}

// PRSigstoreSignedWithRekorPublicKeyPath specifies a value for the "rekorPublicKeyPath" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithRekorPublicKeyPath(rekorPublicKeyPath string) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.RekorPublicKeyPath != "" {
return errors.New(`"rekorPublicKeyPath" already specified`)
}
pr.RekorPublicKeyPath = rekorPublicKeyPath
return nil
}
}

// PRSigstoreSignedWithRekorPublicKeyData specifies a value for the "rekorPublicKeyData" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithRekorPublicKeyData(rekorPublicKeyData []byte) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
if pr.RekorPublicKeyData != nil {
return errors.New(`"rekorPublicKeyData" already specified`)
}
pr.RekorPublicKeyData = rekorPublicKeyData
return nil
}
}

// PRSigstoreSignedWithSignedIdentity specifies a value for the "signedIdentity" field when calling NewPRSigstoreSigned.
func PRSigstoreSignedWithSignedIdentity(signedIdentity PolicyReferenceMatch) PRSigstoreSignedOption {
return func(pr *prSigstoreSigned) error {
Expand All @@ -54,12 +87,28 @@ func newPRSigstoreSigned(options ...PRSigstoreSignedOption) (*prSigstoreSigned,
return nil, err
}
}
if res.KeyPath != "" && res.KeyData != nil {
return nil, InvalidPolicyFormatError("keyType and keyData cannot be used simultaneously")

keySources := 0
if res.KeyPath != "" {
keySources++
}
if res.KeyData != nil {
keySources++
}
if res.KeyPath == "" && res.KeyData == nil {
return nil, InvalidPolicyFormatError("At least one of keyPath and keyData must be specified")
if res.Fulcio != nil {
keySources++
}
if keySources != 1 {
return nil, InvalidPolicyFormatError("exactly one of keyPath, keyData and fulcio must be specified")
}

if res.RekorPublicKeyPath != "" && res.RekorPublicKeyData != nil {
return nil, InvalidPolicyFormatError("rekorPublickeyType and rekorPublickeyData cannot be used simultaneously")
}
if res.Fulcio != nil && res.RekorPublicKeyPath == "" && res.RekorPublicKeyData == nil {
return nil, InvalidPolicyFormatError("At least one of RekorPublickeyPath and RekorPublickeyData must be specified if fulcio is used")
}

if res.SignedIdentity == nil {
return nil, InvalidPolicyFormatError("signedIdentity not specified")
}
Expand Down Expand Up @@ -95,7 +144,8 @@ var _ json.Unmarshaler = (*prSigstoreSigned)(nil)
func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
*pr = prSigstoreSigned{}
var tmp prSigstoreSigned
var gotKeyPath, gotKeyData = false, false
var gotKeyPath, gotKeyData, gotFulcio, gotRekorPublicKeyPath, gotRekorPublicKeyData bool
var fulcio prSigstoreSignedFulcio
var signedIdentity json.RawMessage
if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} {
switch key {
Expand All @@ -107,6 +157,15 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
case "keyData":
gotKeyData = true
return &tmp.KeyData
case "fulcio":
gotFulcio = true
return &fulcio
case "rekorPublicKeyPath":
gotRekorPublicKeyPath = true
return &tmp.RekorPublicKeyPath
case "rekorPublicKeyData":
gotRekorPublicKeyData = true
return &tmp.RekorPublicKeyData
case "signedIdentity":
return &signedIdentity
default:
Expand Down Expand Up @@ -136,13 +195,149 @@ func (pr *prSigstoreSigned) UnmarshalJSON(data []byte) error {
if gotKeyData {
opts = append(opts, PRSigstoreSignedWithKeyData(tmp.KeyData))
}
if gotFulcio {
opts = append(opts, PRSigstoreSignedWithFulcio(&fulcio))
}
if gotRekorPublicKeyPath {
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyPath(tmp.RekorPublicKeyPath))
}
if gotRekorPublicKeyData {
opts = append(opts, PRSigstoreSignedWithRekorPublicKeyData(tmp.RekorPublicKeyData))
}
opts = append(opts, PRSigstoreSignedWithSignedIdentity(tmp.SignedIdentity))

res, err := newPRSigstoreSigned(opts...)
if err != nil {
return err
}
*pr = *res
return nil
}

// PRSigstoreSignedFulcioOption is a way to pass values to NewPRSigstoreSignedFulcio
type PRSigstoreSignedFulcioOption func(*prSigstoreSignedFulcio) error

// PRSigstoreSignedFulcioWithCAPath specifies a value for the "caPath" field when calling NewPRSigstoreSignedFulcio
func PRSigstoreSignedFulcioWithCAPath(caPath string) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.CAPath != "" {
return errors.New(`"caPath" already specified`)
}
f.CAPath = caPath
return nil
}
}

// PRSigstoreSignedFulcioWithCAData specifies a value for the "caData" field when calling NewPRSigstoreSignedFulcio
func PRSigstoreSignedFulcioWithCAData(caData []byte) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.CAData != nil {
return errors.New(`"caData" already specified`)
}
f.CAData = caData
return nil
}
}

// PRSigstoreSignedFulcioWithOIDCIssuer specifies a value for the "oidcIssuer" field when calling NewPRSigstoreSignedFulcio
func PRSigstoreSignedFulcioWithOIDCIssuer(oidcIssuer string) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.OIDCIssuer != "" {
return errors.New(`"oidcIssuer" already specified`)
}
f.OIDCIssuer = oidcIssuer
return nil
}
}

// PRSigstoreSignedFulcioWithSubjectEmail specifies a value for the "subjectEmail" field when calling NewPRSigstoreSignedFulcio
func PRSigstoreSignedFulcioWithSubjectEmail(subjectEmail string) PRSigstoreSignedFulcioOption {
return func(f *prSigstoreSignedFulcio) error {
if f.SubjectEmail != "" {
return errors.New(`"subjectEmail" already specified`)
}
f.SubjectEmail = subjectEmail
return nil
}
}

// newPRSigstoreSignedFulcio is NewPRSigstoreSignedFulcio, except it returns the private type
func newPRSigstoreSignedFulcio(options ...PRSigstoreSignedFulcioOption) (*prSigstoreSignedFulcio, error) {
res := prSigstoreSignedFulcio{}
for _, o := range options {
if err := o(&res); err != nil {
return nil, err
}
}

if res.CAPath != "" && res.CAData != nil {
return nil, InvalidPolicyFormatError("caPath and caData cannot be used simultaneously")
}
if res.CAPath == "" && res.CAData == nil {
return nil, InvalidPolicyFormatError("At least one of caPath and caData must be specified")
}
if res.OIDCIssuer == "" {
return nil, InvalidPolicyFormatError("oidcIssuer not specified")
}
if res.SubjectEmail == "" {
return nil, InvalidPolicyFormatError("subjectEmail not specified")
}

return &res, nil
}

// NewPRSigstoreSignedFulcio returns a PRSigstoreSignedFulcio based on options.
func NewPRSigstoreSignedFulcio(options ...PRSigstoreSignedFulcioOption) (PRSigstoreSignedFulcio, error) {
return newPRSigstoreSignedFulcio(options...)
}

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

func (f *prSigstoreSignedFulcio) UnmarshalJSON(data []byte) error {
*f = prSigstoreSignedFulcio{}
var tmp prSigstoreSignedFulcio
var gotCAPath, gotCAData, gotOIDCIssuer, gotSubjectEmail bool // = false...
if err := internal.ParanoidUnmarshalJSONObject(data, func(key string) interface{} {
switch key {
case "caPath":
gotCAPath = true
return &tmp.CAPath
case "caData":
gotCAData = true
return &tmp.CAData
case "oidcIssuer":
gotOIDCIssuer = true
return &tmp.OIDCIssuer
case "subjectEmail":
gotSubjectEmail = true
return &tmp.SubjectEmail
default:
return nil
}
}); err != nil {
return err
}

var opts []PRSigstoreSignedFulcioOption
if gotCAPath {
opts = append(opts, PRSigstoreSignedFulcioWithCAPath(tmp.CAPath))
}
if gotCAData {
opts = append(opts, PRSigstoreSignedFulcioWithCAData(tmp.CAData))
}
if gotOIDCIssuer {
opts = append(opts, PRSigstoreSignedFulcioWithOIDCIssuer(tmp.OIDCIssuer))
}
if gotSubjectEmail {
opts = append(opts, PRSigstoreSignedFulcioWithSubjectEmail(tmp.SubjectEmail))
}

res, err := newPRSigstoreSignedFulcio(opts...)
if err != nil {
return err
}

*f = *res
return nil
}
Loading

0 comments on commit 23774f5

Please sign in to comment.