mirror of
https://github.com/project-zot/zot.git
synced 2025-01-06 22:40:28 -05:00
ba6f347d8d
Which could be imported independently. See more details: 1. "zotregistry.io/zot/pkg/test/common" - currently used as tcommon "zotregistry.io/zot/pkg/test/common" - inside pkg/test test "zotregistry.io/zot/pkg/test/common" - in tests . "zotregistry.io/zot/pkg/test/common" - in tests Decouple zb from code in test/pkg in order to keep the size small. 2. "zotregistry.io/zot/pkg/test/image-utils" - curently used as . "zotregistry.io/zot/pkg/test/image-utils" 3. "zotregistry.io/zot/pkg/test/deprecated" - curently used as "zotregistry.io/zot/pkg/test/deprecated" This one will bre replaced gradually by image-utils in the future. 4. "zotregistry.io/zot/pkg/test/signature" - (cosign + notation) use as "zotregistry.io/zot/pkg/test/signature" 5. "zotregistry.io/zot/pkg/test/auth" - (bearer + oidc) curently used as authutils "zotregistry.io/zot/pkg/test/auth" 6. "zotregistry.io/zot/pkg/test/oci-utils" - curently used as ociutils "zotregistry.io/zot/pkg/test/oci-utils" Some unused functions were removed, some were replaced, and in a few cases specific funtions were moved to the files they were used in. Added an interface for the StoreController, this reduces the number of imports of the entire image store, decreasing binary size for tests. If the zb code was still coupled with pkg/test, this would have reflected in zb size. Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
469 lines
10 KiB
Go
469 lines
10 KiB
Go
package signature
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"math"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/notaryproject/notation-core-go/signature/jws"
|
|
"github.com/notaryproject/notation-core-go/testhelper"
|
|
"github.com/notaryproject/notation-go"
|
|
notconfig "github.com/notaryproject/notation-go/config"
|
|
"github.com/notaryproject/notation-go/dir"
|
|
notreg "github.com/notaryproject/notation-go/registry"
|
|
"github.com/notaryproject/notation-go/signer"
|
|
"github.com/notaryproject/notation-go/verifier"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"oras.land/oras-go/v2/registry"
|
|
"oras.land/oras-go/v2/registry/remote"
|
|
"oras.land/oras-go/v2/registry/remote/auth"
|
|
|
|
tcommon "zotregistry.io/zot/pkg/test/common"
|
|
)
|
|
|
|
var (
|
|
ErrAlreadyExists = errors.New("already exists")
|
|
ErrKeyNotFound = errors.New("key not found")
|
|
ErrSignatureVerification = errors.New("signature verification failed")
|
|
)
|
|
|
|
var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals
|
|
|
|
func LoadNotationPath(tdir string) {
|
|
dir.UserConfigDir = filepath.Join(tdir, "notation")
|
|
|
|
// set user libexec
|
|
dir.UserLibexecDir = dir.UserConfigDir
|
|
}
|
|
|
|
func GenerateNotationCerts(tdir string, certName string) error {
|
|
// generate RSA private key
|
|
bits := 2048
|
|
|
|
key, err := rsa.GenerateKey(rand.Reader, bits)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyBytes, err := x509.MarshalPKCS8PrivateKey(key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
|
|
|
|
rsaCertTuple := testhelper.GetRSASelfSignedCertTupleWithPK(key, "cert")
|
|
|
|
certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rsaCertTuple.Cert.Raw})
|
|
|
|
// write private key
|
|
relativeKeyPath, relativeCertPath := dir.LocalKeyPath(certName)
|
|
|
|
configFS := dir.ConfigFS()
|
|
|
|
keyPath, err := configFS.SysPath(relativeKeyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
certPath, err := configFS.SysPath(relativeCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := tcommon.WriteFileWithPermission(keyPath, keyPEM, 0o600, false); err != nil { //nolint:gomnd
|
|
return fmt.Errorf("failed to write key file: %w", err)
|
|
}
|
|
|
|
// write self-signed certificate
|
|
if err := tcommon.WriteFileWithPermission(certPath, certBytes, 0o644, false); err != nil { //nolint:gomnd
|
|
return fmt.Errorf("failed to write certificate file: %w", err)
|
|
}
|
|
|
|
signingKeys, err := notconfig.LoadSigningKeys()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
keySuite := notconfig.KeySuite{
|
|
Name: certName,
|
|
X509KeyPair: ¬config.X509KeyPair{
|
|
KeyPath: keyPath,
|
|
CertificatePath: certPath,
|
|
},
|
|
}
|
|
|
|
// addKeyToSigningKeys
|
|
if tcommon.Contains(signingKeys.Keys, keySuite.Name) {
|
|
return ErrAlreadyExists
|
|
}
|
|
|
|
signingKeys.Keys = append(signingKeys.Keys, keySuite)
|
|
|
|
// Add to the trust store
|
|
trustStorePath := path.Join(tdir, fmt.Sprintf("notation/truststore/x509/ca/%s", certName))
|
|
|
|
if _, err := os.Stat(filepath.Join(trustStorePath, filepath.Base(certPath))); err == nil {
|
|
return ErrAlreadyExists
|
|
}
|
|
|
|
if err := os.MkdirAll(trustStorePath, 0o755); err != nil { //nolint:gomnd
|
|
return fmt.Errorf("GenerateNotationCerts os.MkdirAll failed: %w", err)
|
|
}
|
|
|
|
trustCertPath := path.Join(trustStorePath, fmt.Sprintf("%s%s", certName, dir.LocalCertificateExtension))
|
|
|
|
err = tcommon.CopyFile(certPath, trustCertPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Save to the SigningKeys.json
|
|
if err := signingKeys.Save(); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func SignWithNotation(keyName string, reference string, tdir string) error {
|
|
ctx := context.TODO()
|
|
|
|
// getSigner
|
|
var newSigner notation.Signer
|
|
|
|
mediaType := jws.MediaTypeEnvelope
|
|
|
|
// ResolveKey
|
|
signingKeys, err := LoadNotationSigningkeys(tdir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idx := tcommon.Index(signingKeys.Keys, keyName)
|
|
if idx < 0 {
|
|
return ErrKeyNotFound
|
|
}
|
|
|
|
key := signingKeys.Keys[idx]
|
|
|
|
if key.X509KeyPair != nil {
|
|
newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// prepareSigningContent
|
|
// getRepositoryClient
|
|
authClient := &auth.Client{
|
|
Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
|
|
return auth.EmptyCredential, nil
|
|
},
|
|
Cache: auth.NewCache(),
|
|
ClientID: "notation",
|
|
}
|
|
|
|
authClient.SetUserAgent("notation/zot_tests")
|
|
|
|
plainHTTP := true
|
|
|
|
// Resolve referance
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteRepo := &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
repositoryOpts := notreg.RepositoryOptions{}
|
|
|
|
sigRepo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
|
|
|
|
sigOpts := notation.SignOptions{
|
|
SignerSignOptions: notation.SignerSignOptions{
|
|
SignatureMediaType: mediaType,
|
|
PluginConfig: map[string]string{},
|
|
},
|
|
ArtifactReference: ref.String(),
|
|
}
|
|
|
|
_, err = notation.Sign(ctx, newSigner, sigRepo, sigOpts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func VerifyWithNotation(reference string, tdir string) error {
|
|
// check if trustpolicy.json exists
|
|
trustpolicyPath := path.Join(tdir, "notation/trustpolicy.json")
|
|
|
|
if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) {
|
|
trustPolicy := `
|
|
{
|
|
"version": "1.0",
|
|
"trustPolicies": [
|
|
{
|
|
"name": "good",
|
|
"registryScopes": [ "*" ],
|
|
"signatureVerification": {
|
|
"level" : "audit"
|
|
},
|
|
"trustStores": ["ca:good"],
|
|
"trustedIdentities": [
|
|
"*"
|
|
]
|
|
}
|
|
]
|
|
}`
|
|
|
|
file, err := os.Create(trustpolicyPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
_, err = file.WriteString(trustPolicy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// start verifying signatures
|
|
ctx := context.TODO()
|
|
|
|
// getRepositoryClient
|
|
authClient := &auth.Client{
|
|
Credential: func(ctx context.Context, reg string) (auth.Credential, error) {
|
|
return auth.EmptyCredential, nil
|
|
},
|
|
Cache: auth.NewCache(),
|
|
ClientID: "notation",
|
|
}
|
|
|
|
authClient.SetUserAgent("notation/zot_tests")
|
|
|
|
plainHTTP := true
|
|
|
|
// Resolve referance
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteRepo := &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
repositoryOpts := notreg.RepositoryOptions{}
|
|
|
|
repo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
|
|
|
|
manifestDesc, err := repo.Resolve(ctx, ref.Reference)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := ref.ValidateReferenceAsDigest(); err != nil {
|
|
ref.Reference = manifestDesc.Digest.String()
|
|
}
|
|
|
|
// getVerifier
|
|
newVerifier, err := verifier.NewFromConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
remoteRepo = &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
repo = notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts)
|
|
|
|
configs := map[string]string{}
|
|
|
|
verifyOpts := notation.VerifyOptions{
|
|
ArtifactReference: ref.String(),
|
|
PluginConfig: configs,
|
|
MaxSignatureAttempts: math.MaxInt64,
|
|
}
|
|
|
|
_, outcomes, err := notation.Verify(ctx, newVerifier, repo, verifyOpts)
|
|
if err != nil || len(outcomes) == 0 {
|
|
return ErrSignatureVerification
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, error) {
|
|
signatures := []godigest.Digest{}
|
|
|
|
ctx := context.TODO()
|
|
|
|
// getSignatureRepository
|
|
ref, err := registry.ParseReference(reference)
|
|
if err != nil {
|
|
return signatures, err
|
|
}
|
|
|
|
plainHTTP := true
|
|
|
|
// getRepositoryClient
|
|
authClient := &auth.Client{
|
|
Credential: func(ctx context.Context, registry string) (auth.Credential, error) {
|
|
return auth.EmptyCredential, nil
|
|
},
|
|
Cache: auth.NewCache(),
|
|
ClientID: "notation",
|
|
}
|
|
|
|
authClient.SetUserAgent("notation/zot_tests")
|
|
|
|
remoteRepo := &remote.Repository{
|
|
Client: authClient,
|
|
Reference: ref,
|
|
PlainHTTP: plainHTTP,
|
|
}
|
|
|
|
sigRepo := notreg.NewRepository(remoteRepo)
|
|
|
|
artifactDesc, err := sigRepo.Resolve(ctx, reference)
|
|
if err != nil {
|
|
return signatures, err
|
|
}
|
|
|
|
err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error {
|
|
for _, sigManifestDesc := range signatureManifests {
|
|
signatures = append(signatures, sigManifestDesc.Digest)
|
|
}
|
|
|
|
return nil
|
|
})
|
|
|
|
return signatures, err
|
|
}
|
|
|
|
func LoadNotationSigningkeys(tdir string) (*notconfig.SigningKeys, error) {
|
|
var err error
|
|
|
|
var signingKeysInfo *notconfig.SigningKeys
|
|
|
|
filePath := path.Join(tdir, "notation/signingkeys.json")
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
// create file
|
|
newSigningKeys := notconfig.NewSigningKeys()
|
|
|
|
newFile, err := os.Create(filePath)
|
|
if err != nil {
|
|
return newSigningKeys, err
|
|
}
|
|
|
|
defer newFile.Close()
|
|
|
|
encoder := json.NewEncoder(newFile)
|
|
encoder.SetIndent("", " ")
|
|
|
|
err = encoder.Encode(newSigningKeys)
|
|
|
|
return newSigningKeys, err
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
err = json.NewDecoder(file).Decode(&signingKeysInfo)
|
|
|
|
return signingKeysInfo, err
|
|
}
|
|
|
|
func LoadNotationConfig(tdir string) (*notconfig.Config, error) {
|
|
var configInfo *notconfig.Config
|
|
|
|
filePath := path.Join(tdir, "notation/signingkeys.json")
|
|
|
|
file, err := os.Open(filePath)
|
|
if err != nil {
|
|
return configInfo, err
|
|
}
|
|
|
|
defer file.Close()
|
|
|
|
err = json.NewDecoder(file).Decode(&configInfo)
|
|
if err != nil {
|
|
return configInfo, err
|
|
}
|
|
|
|
// set default value
|
|
configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat)
|
|
if configInfo.SignatureFormat == "" {
|
|
configInfo.SignatureFormat = "jws"
|
|
}
|
|
|
|
return configInfo, nil
|
|
}
|
|
|
|
func SignImageUsingNotary(repoTag, port string) error {
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer func() { _ = os.Chdir(cwd) }()
|
|
|
|
tdir, err := os.MkdirTemp("", "notation")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
defer os.RemoveAll(tdir)
|
|
|
|
_ = os.Chdir(tdir)
|
|
|
|
NotationPathLock.Lock()
|
|
defer NotationPathLock.Unlock()
|
|
|
|
LoadNotationPath(tdir)
|
|
|
|
// generate a keypair
|
|
err = GenerateNotationCerts(tdir, "notation-sign-test")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// sign the image
|
|
image := fmt.Sprintf("localhost:%s/%s", port, repoTag)
|
|
|
|
err = SignWithNotation("notation-sign-test", image, tdir)
|
|
|
|
return err
|
|
}
|