refactor(extensions)!: refactor the extensions URLs and errors (#1636)

BREAKING CHANGE: The functionality provided by the mgmt endpoint has beed redesigned - see details below
BREAKING CHANGE: The API keys endpoint has been moved -  see details below
BREAKING CHANGE: The mgmt extension config has been removed - endpoint is now enabled by having both the search and the ui extensions enabled
BREAKING CHANGE: The API keys configuration has been moved from extensions to http>auth>apikey

mgmt and imagetrust extensions:
- separate the _zot/ext/mgmt into 3 separate endpoints: _zot/ext/auth, _zot/ext/notation, _zot/ext/cosign
- signature verification logic is in a separate `imagetrust` extension
- better hanling or errors in case of signature uploads: logging and error codes (more 400 and less 500 errors)
- add authz on signature uploads (and add a new middleware in common for this purpose)
- remove the mgmt extension configuration - it is now enabled if the UI and the search extensions are enabled

userprefs estension:
- userprefs are enabled if both search and ui extensions are enabled (as opposed to just search)

apikey extension is removed and logic moved into the api folder
- Move apikeys code out of pkg/extensions and into pkg/api
- Remove apikey configuration options from the extensions configuration and move it inside the http auth section
- remove the build label apikeys

other changes:
- move most of the logic adding handlers to the extensions endpoints out of routes.go and into the extensions files.
- add warnings in case the users are still using configurations with the obsolete settings for mgmt and api keys
- add a new function in the extension package which could be a single point of starting backgroud tasks for all extensions
- more clear methods for verifying specific extensions are enabled
- fix http methods paired with the UI handlers
- rebuild swagger docs

Signed-off-by: Andrei Aaron <aaaron@luxoft.com>
This commit is contained in:
Andrei Aaron
2023-08-02 21:58:34 +03:00
committed by GitHub
parent 42f9f78125
commit 77149aa85c
61 changed files with 3405 additions and 1471 deletions
-132
View File
@@ -1,132 +0,0 @@
# Verifying signatures
## How to configure zot for verifying signatures
In order to configure zot for verifying signatures, the user should provide:
1. public keys (which correspond to the private keys used to sign images with `cosign`)
or
2. certificates (used to sign images with `notation`)
These files could be uploaded using one of these requests:
1. upload a public key
***Example of request***
```
curl --data-binary @file.pub -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=cosign"
```
2. upload a certificate
***Example of request***
```
curl --data-binary @filet.crt -X POST "http://localhost:8080/v2/_zot/ext/mgmt?resource=signatures&tool=notation&truststoreType=ca&truststoreName=upload-cert"
```
Besides the requested files, the user should also specify the `tool` which should be :
- `cosign` for uploading public keys
- `notation` for uploading certificates
Also, if the uploaded file is a certificate then the user should also specify the type of the truststore through `truststoreType` param and also its name through `truststoreName` param.
Based on the uploaded files, signatures verification will be performed for all the signed images. Then the information known about the signatures will be:
- the tool used to generate the signature (`cosign` or `notation`)
- info about the trustworthiness of the signature (if there is a certificate or a public key which can successfully verify the signature)
- the author of the signature which will be:
- the public key -> for signatures generated using `cosign`
- the subject of the certificate -> for signatures generated using `notation`
**Example of GraphQL output**
```json
{
"data": {
"Image": {
"Manifests": [
{
"Digest":"sha256:6c19fba547b87bde9a45df2f8563e0c61826d098dd30192a2c8b86da1e1a6360"
}
],
"IsSigned": true,
"Tag": "latest",
"SignatureInfo":[
{
"Tool":"cosign",
"IsTrusted":false,
"Author":""
},
{
"Tool":"cosign",
"IsTrusted":false,
"Author":""
},
{
"Tool":"cosign",
"IsTrusted": true,
"Author":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE9pN+/hGcFlh4YYaNvZxNvuh8Qyhl\npURz77qScOHe3DqdmiWiuqIseyhEdjEDwpL6fHRwu3a2Nd9wbKqm0la76w==\n-----END PUBLIC KEY-----\n"
},
{
"Tool":"notation",
"IsTrusted": false,
"Author":"CN=v4-test,O=Notary,L=Seattle,ST=WA,C=US"
},
{
"Tool":"notation",
"IsTrusted": true,
"Author":"CN=multipleSig,O=Notary,L=Seattle,ST=WA,C=US"
}
]
}
}
}
```
## Notes
- The files (public keys and certificates) uploaded using the exposed routes will be stored in some specific directories called `_cosign` and `_notation` under `$rootDir`.
- `_cosign` directory will contain the uploaded public keys
```
_cosign
├── $publicKey1
└── $publicKey2
```
- `_notation` directory will have this structure:
```
_notation
├── trustpolicy.json
└── truststore
└── x509
└── $truststoreType
└── $truststoreName
└── $certificate
```
where `trustpolicy.json` file has this default content which can not be modified by the user and which is updated each time a new certificate is added to a new truststore:
```
{
"version": "1.0",
"trustPolicies": [
{
"name": "default-config",
"registryScopes": [ "*" ],
"signatureVerification": {
"level" : "strict"
},
"trustStores": [],
"trustedIdentities": [
"*"
]
}
]
}
```
+2 -1
View File
@@ -5,6 +5,7 @@ import (
"context"
"crypto"
"encoding/base64"
"fmt"
"io"
"os"
"path"
@@ -136,7 +137,7 @@ func UploadPublicKey(publicKeyContent []byte) error {
func validatePublicKey(publicKeyContent []byte) (bool, error) {
_, err := cryptoutils.UnmarshalPEMToPublicKey(publicKeyContent)
if err != nil {
return false, err
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidPublicKeyContent, err)
}
return true, nil
+4 -3
View File
@@ -302,7 +302,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
// data may be in DER format
derCerts, err := x509.ParseCertificates(certificateContent)
if err != nil {
return false, err
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, derCerts...)
@@ -311,7 +311,7 @@ func validateCertificate(certificateContent []byte) (bool, error) {
for block != nil {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return false, err
return false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err)
}
certs = append(certs, cert)
block, rest = pem.Decode(rest)
@@ -319,7 +319,8 @@ func validateCertificate(certificateContent []byte) (bool, error) {
}
if len(certs) == 0 {
return false, zerr.ErrInvalidCertificateContent
return false, fmt.Errorf("%w: no valid certificates found in payload",
zerr.ErrInvalidCertificateContent)
}
return true, nil
+85
View File
@@ -1,6 +1,7 @@
package signatures
import (
"context"
"encoding/json"
"time"
@@ -8,6 +9,9 @@ import (
ispec "github.com/opencontainers/image-spec/specs-go/v1"
zerr "zotregistry.io/zot/errors"
"zotregistry.io/zot/pkg/log"
mTypes "zotregistry.io/zot/pkg/meta/types"
"zotregistry.io/zot/pkg/scheduler"
)
const (
@@ -58,3 +62,84 @@ func VerifySignature(
return "", time.Time{}, false, zerr.ErrInvalidSignatureType
}
}
func NewTaskGenerator(metaDB mTypes.MetaDB, log log.Logger) scheduler.TaskGenerator {
return &sigValidityTaskGenerator{
repos: []mTypes.RepoMetadata{},
metaDB: metaDB,
repoIndex: -1,
log: log,
}
}
type sigValidityTaskGenerator struct {
repos []mTypes.RepoMetadata
metaDB mTypes.MetaDB
repoIndex int
done bool
log log.Logger
}
func (gen *sigValidityTaskGenerator) Next() (scheduler.Task, error) {
if len(gen.repos) == 0 {
ctx := context.Background()
repos, err := gen.metaDB.GetMultipleRepoMeta(ctx, func(repoMeta mTypes.RepoMetadata) bool {
return true
})
if err != nil {
return nil, err
}
gen.repos = repos
}
gen.repoIndex++
if gen.repoIndex >= len(gen.repos) {
gen.done = true
return nil, nil
}
return NewValidityTask(gen.metaDB, gen.repos[gen.repoIndex], gen.log), nil
}
func (gen *sigValidityTaskGenerator) IsDone() bool {
return gen.done
}
func (gen *sigValidityTaskGenerator) Reset() {
gen.done = false
gen.repoIndex = -1
gen.repos = []mTypes.RepoMetadata{}
}
type validityTask struct {
metaDB mTypes.MetaDB
repo mTypes.RepoMetadata
log log.Logger
}
func NewValidityTask(metaDB mTypes.MetaDB, repo mTypes.RepoMetadata, log log.Logger) *validityTask {
return &validityTask{metaDB, repo, log}
}
func (validityT *validityTask) DoWork() error {
validityT.log.Info().Msg("updating signatures validity")
for signedManifest, sigs := range validityT.repo.Signatures {
if len(sigs[CosignSignature]) != 0 || len(sigs[NotationSignature]) != 0 {
err := validityT.metaDB.UpdateSignaturesValidity(validityT.repo.Name, godigest.Digest(signedManifest))
if err != nil {
validityT.log.Info().Msg("error while verifying signatures")
return err
}
}
}
validityT.log.Info().Msg("verifying signatures successfully completed")
return nil
}