zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/test/signature/notation.go (about) 1 package signature 2 3 import ( 4 "context" 5 "crypto/rand" 6 "crypto/rsa" 7 "crypto/x509" 8 "encoding/json" 9 "encoding/pem" 10 "errors" 11 "fmt" 12 "io/fs" 13 "math" 14 "os" 15 "path" 16 "path/filepath" 17 "strings" 18 "sync" 19 20 "github.com/notaryproject/notation-core-go/signature/jws" 21 "github.com/notaryproject/notation-core-go/testhelper" 22 "github.com/notaryproject/notation-go" 23 notconfig "github.com/notaryproject/notation-go/config" 24 "github.com/notaryproject/notation-go/dir" 25 notreg "github.com/notaryproject/notation-go/registry" 26 "github.com/notaryproject/notation-go/signer" 27 "github.com/notaryproject/notation-go/verifier" 28 godigest "github.com/opencontainers/go-digest" 29 ispec "github.com/opencontainers/image-spec/specs-go/v1" 30 "oras.land/oras-go/v2/registry" 31 "oras.land/oras-go/v2/registry/remote" 32 "oras.land/oras-go/v2/registry/remote/auth" 33 34 tcommon "zotregistry.dev/zot/pkg/test/common" 35 ) 36 37 var ( 38 ErrAlreadyExists = errors.New("already exists") 39 ErrKeyNotFound = errors.New("key not found") 40 ErrSignatureVerification = errors.New("signature verification failed") 41 ) 42 43 var NotationPathLock = new(sync.Mutex) //nolint: gochecknoglobals 44 45 func LoadNotationPath(tdir string) { 46 dir.UserConfigDir = filepath.Join(tdir, "notation") 47 48 // set user libexec 49 dir.UserLibexecDir = dir.UserConfigDir 50 } 51 52 func GenerateNotationCerts(tdir string, certName string) error { 53 // generate RSA private key 54 bits := 2048 55 56 key, err := rsa.GenerateKey(rand.Reader, bits) 57 if err != nil { 58 return err 59 } 60 61 keyBytes, err := x509.MarshalPKCS8PrivateKey(key) 62 if err != nil { 63 return err 64 } 65 66 keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) 67 68 rsaCertTuple := testhelper.GetRSASelfSignedCertTupleWithPK(key, "cert") 69 70 certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rsaCertTuple.Cert.Raw}) 71 72 // write private key 73 relativeKeyPath, relativeCertPath := dir.LocalKeyPath(certName) 74 75 configFS := dir.ConfigFS() 76 77 keyPath, err := configFS.SysPath(relativeKeyPath) 78 if err != nil { 79 return err 80 } 81 82 certPath, err := configFS.SysPath(relativeCertPath) 83 if err != nil { 84 return err 85 } 86 87 if err := tcommon.WriteFileWithPermission(keyPath, keyPEM, 0o600, false); err != nil { //nolint:gomnd 88 return fmt.Errorf("failed to write key file: %w", err) 89 } 90 91 // write self-signed certificate 92 if err := tcommon.WriteFileWithPermission(certPath, certBytes, 0o644, false); err != nil { //nolint:gomnd 93 return fmt.Errorf("failed to write certificate file: %w", err) 94 } 95 96 signingKeys, err := notconfig.LoadSigningKeys() 97 if err != nil { 98 return err 99 } 100 101 keySuite := notconfig.KeySuite{ 102 Name: certName, 103 X509KeyPair: ¬config.X509KeyPair{ 104 KeyPath: keyPath, 105 CertificatePath: certPath, 106 }, 107 } 108 109 // addKeyToSigningKeys 110 if tcommon.Contains(signingKeys.Keys, keySuite.Name) { 111 return ErrAlreadyExists 112 } 113 114 signingKeys.Keys = append(signingKeys.Keys, keySuite) 115 116 // Add to the trust store 117 trustStorePath := path.Join(tdir, fmt.Sprintf("notation/truststore/x509/ca/%s", certName)) 118 119 if _, err := os.Stat(filepath.Join(trustStorePath, filepath.Base(certPath))); err == nil { 120 return ErrAlreadyExists 121 } 122 123 if err := os.MkdirAll(trustStorePath, 0o755); err != nil { //nolint:gomnd 124 return fmt.Errorf("GenerateNotationCerts os.MkdirAll failed: %w", err) 125 } 126 127 trustCertPath := path.Join(trustStorePath, fmt.Sprintf("%s%s", certName, dir.LocalCertificateExtension)) 128 129 err = tcommon.CopyFile(certPath, trustCertPath) 130 if err != nil { 131 return err 132 } 133 134 // Save to the SigningKeys.json 135 if err := signingKeys.Save(); err != nil { 136 return err 137 } 138 139 return nil 140 } 141 142 func SignWithNotation(keyName, reference, tdir string, referrersCapability bool) error { 143 ctx := context.TODO() 144 145 // getSigner 146 var newSigner notation.Signer 147 148 mediaType := jws.MediaTypeEnvelope 149 150 // ResolveKey 151 signingKeys, err := LoadNotationSigningkeys(tdir) 152 if err != nil { 153 return err 154 } 155 156 idx := tcommon.Index(signingKeys.Keys, keyName) 157 if idx < 0 { 158 return ErrKeyNotFound 159 } 160 161 key := signingKeys.Keys[idx] 162 163 if key.X509KeyPair != nil { 164 newSigner, err = signer.NewFromFiles(key.X509KeyPair.KeyPath, key.X509KeyPair.CertificatePath) 165 if err != nil { 166 return err 167 } 168 } 169 170 // prepareSigningContent 171 // getRepositoryClient 172 authClient := &auth.Client{ 173 Credential: func(ctx context.Context, reg string) (auth.Credential, error) { 174 return auth.EmptyCredential, nil 175 }, 176 Cache: auth.NewCache(), 177 ClientID: "notation", 178 } 179 180 authClient.SetUserAgent("notation/zot_tests") 181 182 plainHTTP := true 183 184 // Resolve referance 185 ref, err := registry.ParseReference(reference) 186 if err != nil { 187 return err 188 } 189 190 remoteRepo := &remote.Repository{ 191 Client: authClient, 192 Reference: ref, 193 PlainHTTP: plainHTTP, 194 } 195 196 if !referrersCapability { 197 _ = remoteRepo.SetReferrersCapability(false) 198 } 199 200 repositoryOpts := notreg.RepositoryOptions{} 201 202 sigRepo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts) 203 204 sigOpts := notation.SignOptions{ 205 SignerSignOptions: notation.SignerSignOptions{ 206 SignatureMediaType: mediaType, 207 PluginConfig: map[string]string{}, 208 }, 209 ArtifactReference: ref.String(), 210 } 211 212 _, err = notation.Sign(ctx, newSigner, sigRepo, sigOpts) 213 if err != nil { 214 return err 215 } 216 217 return nil 218 } 219 220 func VerifyWithNotation(reference string, tdir string) error { 221 // check if trustpolicy.json exists 222 trustpolicyPath := path.Join(tdir, "notation/trustpolicy.json") 223 224 if _, err := os.Stat(trustpolicyPath); errors.Is(err, os.ErrNotExist) { 225 trustPolicy := ` 226 { 227 "version": "1.0", 228 "trustPolicies": [ 229 { 230 "name": "good", 231 "registryScopes": [ "*" ], 232 "signatureVerification": { 233 "level" : "audit" 234 }, 235 "trustStores": ["ca:good"], 236 "trustedIdentities": [ 237 "*" 238 ] 239 } 240 ] 241 }` 242 243 file, err := os.Create(trustpolicyPath) 244 if err != nil { 245 return err 246 } 247 248 defer file.Close() 249 250 _, err = file.WriteString(trustPolicy) 251 if err != nil { 252 return err 253 } 254 } 255 256 // start verifying signatures 257 ctx := context.TODO() 258 259 // getRepositoryClient 260 authClient := &auth.Client{ 261 Credential: func(ctx context.Context, reg string) (auth.Credential, error) { 262 return auth.EmptyCredential, nil 263 }, 264 Cache: auth.NewCache(), 265 ClientID: "notation", 266 } 267 268 authClient.SetUserAgent("notation/zot_tests") 269 270 plainHTTP := true 271 272 // Resolve referance 273 ref, err := registry.ParseReference(reference) 274 if err != nil { 275 return err 276 } 277 278 remoteRepo := &remote.Repository{ 279 Client: authClient, 280 Reference: ref, 281 PlainHTTP: plainHTTP, 282 } 283 284 repositoryOpts := notreg.RepositoryOptions{} 285 286 repo := notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts) 287 288 manifestDesc, err := repo.Resolve(ctx, ref.Reference) 289 if err != nil { 290 return err 291 } 292 293 if err := ref.ValidateReferenceAsDigest(); err != nil { 294 ref.Reference = manifestDesc.Digest.String() 295 } 296 297 // getVerifier 298 newVerifier, err := verifier.NewFromConfig() 299 if err != nil { 300 return err 301 } 302 303 remoteRepo = &remote.Repository{ 304 Client: authClient, 305 Reference: ref, 306 PlainHTTP: plainHTTP, 307 } 308 309 repo = notreg.NewRepositoryWithOptions(remoteRepo, repositoryOpts) 310 311 configs := map[string]string{} 312 313 verifyOpts := notation.VerifyOptions{ 314 ArtifactReference: ref.String(), 315 PluginConfig: configs, 316 MaxSignatureAttempts: math.MaxInt64, 317 } 318 319 _, outcomes, err := notation.Verify(ctx, newVerifier, repo, verifyOpts) 320 if err != nil || len(outcomes) == 0 { 321 return ErrSignatureVerification 322 } 323 324 return nil 325 } 326 327 func ListNotarySignatures(reference string, tdir string) ([]godigest.Digest, error) { 328 signatures := []godigest.Digest{} 329 330 ctx := context.TODO() 331 332 // getSignatureRepository 333 ref, err := registry.ParseReference(reference) 334 if err != nil { 335 return signatures, err 336 } 337 338 plainHTTP := true 339 340 // getRepositoryClient 341 authClient := &auth.Client{ 342 Credential: func(ctx context.Context, registry string) (auth.Credential, error) { 343 return auth.EmptyCredential, nil 344 }, 345 Cache: auth.NewCache(), 346 ClientID: "notation", 347 } 348 349 authClient.SetUserAgent("notation/zot_tests") 350 351 remoteRepo := &remote.Repository{ 352 Client: authClient, 353 Reference: ref, 354 PlainHTTP: plainHTTP, 355 } 356 357 sigRepo := notreg.NewRepository(remoteRepo) 358 359 artifactDesc, err := sigRepo.Resolve(ctx, reference) 360 if err != nil { 361 return signatures, err 362 } 363 364 err = sigRepo.ListSignatures(ctx, artifactDesc, func(signatureManifests []ispec.Descriptor) error { 365 for _, sigManifestDesc := range signatureManifests { 366 signatures = append(signatures, sigManifestDesc.Digest) 367 } 368 369 return nil 370 }) 371 372 return signatures, err 373 } 374 375 func LoadNotationSigningkeys(tdir string) (*notconfig.SigningKeys, error) { 376 var err error 377 378 var signingKeysInfo *notconfig.SigningKeys 379 380 filePath := path.Join(tdir, "notation/signingkeys.json") 381 382 file, err := os.Open(filePath) 383 if err != nil { 384 if errors.Is(err, fs.ErrNotExist) { 385 // create file 386 newSigningKeys := notconfig.NewSigningKeys() 387 388 newFile, err := os.Create(filePath) 389 if err != nil { 390 return newSigningKeys, err 391 } 392 393 defer newFile.Close() 394 395 encoder := json.NewEncoder(newFile) 396 encoder.SetIndent("", " ") 397 398 err = encoder.Encode(newSigningKeys) 399 400 return newSigningKeys, err 401 } 402 403 return nil, err 404 } 405 406 defer file.Close() 407 408 err = json.NewDecoder(file).Decode(&signingKeysInfo) 409 410 return signingKeysInfo, err 411 } 412 413 func LoadNotationConfig(tdir string) (*notconfig.Config, error) { 414 var configInfo *notconfig.Config 415 416 filePath := path.Join(tdir, "notation/signingkeys.json") 417 418 file, err := os.Open(filePath) 419 if err != nil { 420 return configInfo, err 421 } 422 423 defer file.Close() 424 425 err = json.NewDecoder(file).Decode(&configInfo) 426 if err != nil { 427 return configInfo, err 428 } 429 430 // set default value 431 configInfo.SignatureFormat = strings.ToLower(configInfo.SignatureFormat) 432 if configInfo.SignatureFormat == "" { 433 configInfo.SignatureFormat = "jws" 434 } 435 436 return configInfo, nil 437 } 438 439 func SignImageUsingNotary(repoTag, port string, referrersCapability bool) error { 440 cwd, err := os.Getwd() 441 if err != nil { 442 return err 443 } 444 445 defer func() { _ = os.Chdir(cwd) }() 446 447 tdir, err := os.MkdirTemp("", "notation") 448 if err != nil { 449 return err 450 } 451 452 defer os.RemoveAll(tdir) 453 454 _ = os.Chdir(tdir) 455 456 NotationPathLock.Lock() 457 defer NotationPathLock.Unlock() 458 459 LoadNotationPath(tdir) 460 461 // generate a keypair 462 err = GenerateNotationCerts(tdir, "notation-sign-test") 463 if err != nil { 464 return err 465 } 466 467 // sign the image 468 image := fmt.Sprintf("localhost:%s/%s", port, repoTag) 469 470 err = SignWithNotation("notation-sign-test", image, tdir, referrersCapability) 471 472 return err 473 }