Version 5.22 introduced a new option to /etc/containers/policy.json called
keyPaths, see
https://github.com/containers/image/pull/1609
EL9 immediately took advantage of this new feature and started using it, see
04645c4a84
This quickly became an issue in our code: The go library (containers/image)
parses the configuration file very strictly and refuses to create a client
when policy.json with an unknown key is present on the filesystem. As we
used 5.21.1 that doesn't know the new key, our unit tests started to
failing when containers-common was present.
Reproducer:
podman run --pull=always --rm -it centos:stream9
dnf install -y dnf-plugins-core
dnf config-manager --set-enabled crb
dnf install -y gpgme-devel libassuan-devel krb5-devel golang git-core
git clone https://github.com/osbuild/osbuild-composer
cd osbuild-composer
# install the new containers-common and run the test
dnf install -y https://kojihub.stream.centos.org/kojifiles/packages/containers-common/1/44.el9/x86_64/containers-common-1-44.el9.x86_64.rpm
go test -count 1 ./...
# this returns:
--- FAIL: TestClientResolve (0.00s)
client_test.go:31:
Error Trace: client_test.go:31
Error: Received unexpected error:
Unknown key "keyPaths"
invalid policy in "/etc/containers/policy.json"
github.com/containers/image/v5/signature.NewPolicyFromFile
/osbuild-composer/vendor/github.com/containers/image/v5/signature/policy_config.go:88
github.com/osbuild/osbuild-composer/internal/container.NewClient
/osbuild-composer/internal/container/client.go:123
github.com/osbuild/osbuild-composer/internal/container_test.TestClientResolve
/osbuild-composer/internal/container/client_test.go:29
testing.tRunner
/usr/lib/golang/src/testing/testing.go:1439
runtime.goexit
/usr/lib/golang/src/runtime/asm_amd64.s:1571
Test: TestClientResolve
client_test.go:32:
Error Trace: client_test.go:32
Error: Expected value not to be nil.
Test: TestClientResolve
When run with an older containers-common, it succeeds:
dnf install -y https://kojihub.stream.centos.org/kojifiles/packages/containers-common/1/40.el9/x86_64/containers-common-1-40.el9.x86_64.rpm
go test -count 1 ./...
PASS
To sum it up, I had to upgrade github.com/containers/image/v5 to v5.22.0.
Unfortunately, this wasn't so simple, see
go get github.com/containers/image/v5@latest
go: github.com/containers/image/v5@v5.22.0 requires
github.com/letsencrypt/boulder@v0.0.0-20220331220046-b23ab962616e requires
github.com/honeycombio/beeline-go@v1.1.1 requires
github.com/gobuffalo/pop/v5@v5.3.1 requires
github.com/mattn/go-sqlite3@v2.0.3+incompatible: reading github.com/mattn/go-sqlite3/go.mod at revision v2.0.3: unknown revision v2.0.3
It turns out that github.com/mattn/go-sqlite3@v2.0.3+incompatible has been
recently retracted https://github.com/mattn/go-sqlite3/pull/998 and this
broke a ton of packages depending on it. I was able to fix it by adding
exclude github.com/mattn/go-sqlite3 v2.0.3+incompatible
to our go.mod, see
https://github.com/mattn/go-sqlite3/issues/975#issuecomment-955661657
After adding it,
go get github.com/containers/image/v5@latest
succeeded and tools/prepare-source.sh took care of the rest.
Signed-off-by: Ondřej Budai <ondrej@budai.cz>
102 lines
3.2 KiB
Go
102 lines
3.2 KiB
Go
// Copyright 2015 The Go Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style
|
|
// license that can be found in the LICENSE file.
|
|
|
|
package google
|
|
|
|
import (
|
|
"crypto/rsa"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
"golang.org/x/oauth2/internal"
|
|
"golang.org/x/oauth2/jws"
|
|
)
|
|
|
|
// JWTAccessTokenSourceFromJSON uses a Google Developers service account JSON
|
|
// key file to read the credentials that authorize and authenticate the
|
|
// requests, and returns a TokenSource that does not use any OAuth2 flow but
|
|
// instead creates a JWT and sends that as the access token.
|
|
// The audience is typically a URL that specifies the scope of the credentials.
|
|
//
|
|
// Note that this is not a standard OAuth flow, but rather an
|
|
// optimization supported by a few Google services.
|
|
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
|
|
func JWTAccessTokenSourceFromJSON(jsonKey []byte, audience string) (oauth2.TokenSource, error) {
|
|
return newJWTSource(jsonKey, audience, nil)
|
|
}
|
|
|
|
// JWTAccessTokenSourceWithScope uses a Google Developers service account JSON
|
|
// key file to read the credentials that authorize and authenticate the
|
|
// requests, and returns a TokenSource that does not use any OAuth2 flow but
|
|
// instead creates a JWT and sends that as the access token.
|
|
// The scope is typically a list of URLs that specifies the scope of the
|
|
// credentials.
|
|
//
|
|
// Note that this is not a standard OAuth flow, but rather an
|
|
// optimization supported by a few Google services.
|
|
// Unless you know otherwise, you should use JWTConfigFromJSON instead.
|
|
func JWTAccessTokenSourceWithScope(jsonKey []byte, scope ...string) (oauth2.TokenSource, error) {
|
|
return newJWTSource(jsonKey, "", scope)
|
|
}
|
|
|
|
func newJWTSource(jsonKey []byte, audience string, scopes []string) (oauth2.TokenSource, error) {
|
|
if len(scopes) == 0 && audience == "" {
|
|
return nil, fmt.Errorf("google: missing scope/audience for JWT access token")
|
|
}
|
|
|
|
cfg, err := JWTConfigFromJSON(jsonKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("google: could not parse JSON key: %v", err)
|
|
}
|
|
pk, err := internal.ParseKey(cfg.PrivateKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("google: could not parse key: %v", err)
|
|
}
|
|
ts := &jwtAccessTokenSource{
|
|
email: cfg.Email,
|
|
audience: audience,
|
|
scopes: scopes,
|
|
pk: pk,
|
|
pkID: cfg.PrivateKeyID,
|
|
}
|
|
tok, err := ts.Token()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rts := newErrWrappingTokenSource(oauth2.ReuseTokenSource(tok, ts))
|
|
return rts, nil
|
|
}
|
|
|
|
type jwtAccessTokenSource struct {
|
|
email, audience string
|
|
scopes []string
|
|
pk *rsa.PrivateKey
|
|
pkID string
|
|
}
|
|
|
|
func (ts *jwtAccessTokenSource) Token() (*oauth2.Token, error) {
|
|
iat := time.Now()
|
|
exp := iat.Add(time.Hour)
|
|
scope := strings.Join(ts.scopes, " ")
|
|
cs := &jws.ClaimSet{
|
|
Iss: ts.email,
|
|
Sub: ts.email,
|
|
Aud: ts.audience,
|
|
Scope: scope,
|
|
Iat: iat.Unix(),
|
|
Exp: exp.Unix(),
|
|
}
|
|
hdr := &jws.Header{
|
|
Algorithm: "RS256",
|
|
Typ: "JWT",
|
|
KeyID: string(ts.pkID),
|
|
}
|
|
msg, err := jws.Encode(hdr, cs, ts.pk)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("google: could not encode JWT: %v", err)
|
|
}
|
|
return &oauth2.Token{AccessToken: msg, TokenType: "Bearer", Expiry: exp}, nil
|
|
}
|