zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/imagetrust/notation.go (about) 1 //go:build imagetrust 2 // +build imagetrust 3 4 package imagetrust 5 6 import ( 7 "bytes" 8 "context" 9 "crypto/x509" 10 "encoding/base64" 11 "encoding/json" 12 "encoding/pem" 13 "errors" 14 "fmt" 15 "os" 16 "path" 17 "path/filepath" 18 "regexp" 19 "strings" 20 "time" 21 22 "github.com/aws/aws-sdk-go-v2/service/secretsmanager" 23 "github.com/aws/aws-sdk-go-v2/service/secretsmanager/types" 24 _ "github.com/notaryproject/notation-core-go/signature/jws" 25 "github.com/notaryproject/notation-go" 26 "github.com/notaryproject/notation-go/dir" 27 "github.com/notaryproject/notation-go/plugin" 28 "github.com/notaryproject/notation-go/verifier" 29 "github.com/notaryproject/notation-go/verifier/trustpolicy" 30 "github.com/notaryproject/notation-go/verifier/truststore" 31 godigest "github.com/opencontainers/go-digest" 32 ispec "github.com/opencontainers/image-spec/specs-go/v1" 33 34 zerr "zotregistry.dev/zot/errors" 35 ) 36 37 const ( 38 notationDirRelativePath = "_notation" 39 truststoreName = "default" 40 ) 41 42 type CertificateLocalStorage struct { 43 notationDir string 44 } 45 46 type CertificateAWSStorage struct { 47 secretsManagerClient SecretsManagerClient 48 secretsManagerCache SecretsManagerCache 49 } 50 51 type certificateStorage interface { 52 LoadTrustPolicyDocument() (*trustpolicy.Document, error) 53 StoreCertificate(certificateContent []byte, truststoreType string) error 54 GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) 55 InitTrustpolicy(trustpolicy []byte) error 56 } 57 58 func NewCertificateLocalStorage(rootDir string) (*CertificateLocalStorage, error) { 59 dir := path.Join(rootDir, notationDirRelativePath) 60 61 _, err := os.Stat(dir) 62 if os.IsNotExist(err) { 63 err = os.MkdirAll(dir, defaultDirPerms) 64 if err != nil { 65 return nil, err 66 } 67 } 68 69 if err != nil { 70 return nil, err 71 } 72 73 certStorage := &CertificateLocalStorage{ 74 notationDir: dir, 75 } 76 77 if err := InitTrustpolicyFile(certStorage); err != nil { 78 return nil, err 79 } 80 81 for _, truststoreType := range truststore.Types { 82 defaultTruststore := path.Join(dir, "truststore", "x509", string(truststoreType), truststoreName) 83 84 _, err = os.Stat(defaultTruststore) 85 if os.IsNotExist(err) { 86 err = os.MkdirAll(defaultTruststore, defaultDirPerms) 87 if err != nil { 88 return nil, err 89 } 90 } 91 92 if err != nil { 93 return nil, err 94 } 95 } 96 97 return certStorage, nil 98 } 99 100 func NewCertificateAWSStorage( 101 secretsManagerClient SecretsManagerClient, secretsManagerCache SecretsManagerCache, 102 ) (*CertificateAWSStorage, error) { 103 certStorage := &CertificateAWSStorage{ 104 secretsManagerClient: secretsManagerClient, 105 secretsManagerCache: secretsManagerCache, 106 } 107 108 err := InitTrustpolicyFile(certStorage) 109 if err != nil { 110 return nil, err 111 } 112 113 return certStorage, nil 114 } 115 116 func InitTrustpolicyFile(notationStorage certificateStorage) error { 117 truststores := []string{} 118 119 for _, truststoreType := range truststore.Types { 120 truststores = append(truststores, fmt.Sprintf("\"%s:%s\"", string(truststoreType), truststoreName)) 121 } 122 123 defaultTruststores := strings.Join(truststores, ",") 124 125 // according to https://github.com/notaryproject/notation/blob/main/specs/commandline/verify.md 126 // the value of signatureVerification.level field from trustpolicy.json file 127 // could be one of these values: `strict`, `permissive`, `audit` or `skip` 128 // this default trustpolicy.json file sets the signatureVerification.level 129 // to `strict` which enforces all validations (this means that even if there is 130 // a certificate that verifies a signature, but that certificate has expired, then the 131 // signature is not trusted; if this field were set to `permissive` then the 132 // signature would be trusted) 133 trustPolicy := `{ 134 "version": "1.0", 135 "trustPolicies": [ 136 { 137 "name": "default-config", 138 "registryScopes": [ "*" ], 139 "signatureVerification": { 140 "level" : "strict" 141 }, 142 "trustStores": [` + defaultTruststores + `], 143 "trustedIdentities": [ 144 "*" 145 ] 146 } 147 ] 148 }` 149 150 return notationStorage.InitTrustpolicy([]byte(trustPolicy)) 151 } 152 153 func (local *CertificateLocalStorage) InitTrustpolicy(trustpolicy []byte) error { 154 notationDir, err := local.GetNotationDirPath() 155 if err != nil { 156 return err 157 } 158 159 return os.WriteFile(path.Join(notationDir, dir.PathTrustPolicy), trustpolicy, defaultDirPerms) 160 } 161 162 func (cloud *CertificateAWSStorage) InitTrustpolicy(trustpolicy []byte) error { 163 name := "trustpolicy" 164 description := "notation trustpolicy file" 165 secret := base64.StdEncoding.EncodeToString(trustpolicy) 166 secretInputParam := &secretsmanager.CreateSecretInput{ 167 Name: &name, 168 Description: &description, 169 SecretString: &secret, 170 } 171 172 _, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) 173 if err != nil && IsResourceExistsException(err) { 174 trustpolicyContent, err := cloud.secretsManagerCache.GetSecretString(name) 175 if err != nil { 176 return err 177 } 178 179 existingTrustpolicy, err := base64.StdEncoding.DecodeString(trustpolicyContent) 180 if err == nil && bytes.Equal(trustpolicy, existingTrustpolicy) { 181 return nil 182 } 183 184 force := true 185 186 deleteSecretParam := &secretsmanager.DeleteSecretInput{ 187 SecretId: &name, 188 ForceDeleteWithoutRecovery: &force, 189 } 190 191 _, err = cloud.secretsManagerClient.DeleteSecret(context.Background(), deleteSecretParam) 192 193 if err != nil { 194 return err 195 } 196 197 err = cloud.recreateSecret(secretInputParam) 198 199 return err 200 } 201 202 return err 203 } 204 205 func (cloud *CertificateAWSStorage) recreateSecret(secretInputParam *secretsmanager.CreateSecretInput) error { 206 maxAttempts := 5 207 retrySec := 2 208 209 var err error 210 211 for i := 0; i < maxAttempts; i++ { 212 _, err = cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) 213 214 if err == nil { 215 return nil 216 } 217 218 time.Sleep(time.Duration(retrySec) * time.Second) 219 } 220 221 return err 222 } 223 224 func (local *CertificateLocalStorage) GetNotationDirPath() (string, error) { 225 if local.notationDir != "" { 226 return local.notationDir, nil 227 } 228 229 return "", zerr.ErrSignConfigDirNotSet 230 } 231 232 func (cloud *CertificateAWSStorage) GetCertificates( 233 ctx context.Context, storeType truststore.Type, namedStore string, 234 ) ([]*x509.Certificate, error) { 235 certificates := []*x509.Certificate{} 236 237 if !validateTruststoreType(string(storeType)) { 238 return []*x509.Certificate{}, zerr.ErrInvalidTruststoreType 239 } 240 241 if !validateTruststoreName(namedStore) { 242 return []*x509.Certificate{}, zerr.ErrInvalidTruststoreName 243 } 244 245 listSecretsInput := secretsmanager.ListSecretsInput{ 246 Filters: []types.Filter{ 247 { 248 Key: types.FilterNameStringTypeName, 249 Values: []string{path.Join(string(storeType), namedStore)}, 250 }, 251 }, 252 } 253 254 secrets, err := cloud.secretsManagerClient.ListSecrets(ctx, &listSecretsInput) 255 if err != nil { 256 return []*x509.Certificate{}, err 257 } 258 259 for _, secret := range secrets.SecretList { 260 // get key 261 raw, err := cloud.secretsManagerCache.GetSecretString(*(secret.Name)) 262 if err != nil { 263 return []*x509.Certificate{}, err 264 } 265 266 rawDecoded, err := base64.StdEncoding.DecodeString(raw) 267 if err != nil { 268 return []*x509.Certificate{}, err 269 } 270 271 certs, _, err := parseAndValidateCertificateContent(rawDecoded) 272 if err != nil { 273 return []*x509.Certificate{}, err 274 } 275 276 err = truststore.ValidateCertificates(certs) 277 if err != nil { 278 return []*x509.Certificate{}, err 279 } 280 281 certificates = append(certificates, certs...) 282 } 283 284 return certificates, nil 285 } 286 287 // Equivalent function for trustpolicy.LoadDocument() but using a specific SysFS not the one returned by ConfigFS(). 288 func (local *CertificateLocalStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) { 289 notationDir, err := local.GetNotationDirPath() 290 if err != nil { 291 return nil, err 292 } 293 294 jsonFile, err := dir.NewSysFS(notationDir).Open(dir.PathTrustPolicy) 295 if err != nil { 296 return nil, err 297 } 298 299 defer jsonFile.Close() 300 301 policyDocument := &trustpolicy.Document{} 302 303 err = json.NewDecoder(jsonFile).Decode(policyDocument) 304 if err != nil { 305 return nil, err 306 } 307 308 return policyDocument, nil 309 } 310 311 func (cloud *CertificateAWSStorage) LoadTrustPolicyDocument() (*trustpolicy.Document, error) { 312 policyDocument := &trustpolicy.Document{} 313 314 raw, err := cloud.secretsManagerCache.GetSecretString("trustpolicy") 315 if err != nil { 316 return nil, err 317 } 318 319 rawDecoded, err := base64.StdEncoding.DecodeString(raw) 320 if err != nil { 321 return nil, err 322 } 323 324 var buf bytes.Buffer 325 326 err = json.Compact(&buf, rawDecoded) 327 if err != nil { 328 return nil, err 329 } 330 331 err = json.Unmarshal(buf.Bytes(), policyDocument) 332 if err != nil { 333 return nil, err 334 } 335 336 return policyDocument, nil 337 } 338 339 // NewFromConfig returns a verifier based on local file system. 340 // Equivalent function for verifier.NewFromConfig() 341 // but using LoadTrustPolicyDocumnt() function instead of trustpolicy.LoadDocument() function. 342 func NewFromConfig(notationStorage certificateStorage) (notation.Verifier, error) { 343 // Load trust policy. 344 policyDocument, err := notationStorage.LoadTrustPolicyDocument() 345 if err != nil { 346 return nil, err 347 } 348 349 return notationStorage.GetVerifier(policyDocument) 350 } 351 352 func (local *CertificateLocalStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) { 353 notationDir, err := local.GetNotationDirPath() 354 if err != nil { 355 return nil, err 356 } 357 x509TrustStore := truststore.NewX509TrustStore(dir.NewSysFS(notationDir)) 358 359 return verifier.New(policyDoc, x509TrustStore, 360 plugin.NewCLIManager(dir.NewSysFS(path.Join(notationDir, dir.PathPlugins)))) 361 } 362 363 func (cloud *CertificateAWSStorage) GetVerifier(policyDoc *trustpolicy.Document) (notation.Verifier, error) { 364 return verifier.New(policyDoc, cloud, 365 plugin.NewCLIManager(dir.NewSysFS(path.Join(dir.PathPlugins)))) 366 } 367 368 func VerifyNotationSignature( 369 notationStorage certificateStorage, artifactDescriptor ispec.Descriptor, artifactReference string, 370 rawSignature []byte, signatureMediaType string, 371 ) (string, time.Time, bool, error) { 372 var ( 373 date time.Time 374 author string 375 ) 376 377 // If there's no signature associated with the reference. 378 if len(rawSignature) == 0 { 379 return author, date, false, notation.ErrorSignatureRetrievalFailed{ 380 Msg: fmt.Sprintf("no signature associated with %q is provided, make sure the image was signed successfully", 381 artifactReference), 382 } 383 } 384 385 // Initialize verifier. 386 verifier, err := NewFromConfig(notationStorage) 387 if err != nil { 388 return author, date, false, err 389 } 390 391 ctx := context.Background() 392 393 // Set VerifyOptions. 394 opts := notation.VerifierVerifyOptions{ 395 // ArtifactReference is important to validate registry scope format 396 // If "registryScopes" field from trustpolicy.json file is not wildcard then "domain:80/repo@" should not be hardcoded 397 ArtifactReference: "domain:80/repo@" + artifactReference, 398 SignatureMediaType: signatureMediaType, 399 PluginConfig: map[string]string{}, 400 } 401 402 // Verify the notation signature which should be associated with the artifactDescriptor. 403 outcome, err := verifier.Verify(ctx, artifactDescriptor, rawSignature, opts) 404 if outcome.EnvelopeContent != nil { 405 author = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].Subject.String() 406 407 if outcome.VerificationLevel == trustpolicy.LevelStrict && (err == nil || 408 CheckExpiryErr(outcome.VerificationResults, outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter, err)) { 409 expiry := outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry 410 if !expiry.IsZero() && expiry.Before(outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter) { 411 date = outcome.EnvelopeContent.SignerInfo.SignedAttributes.Expiry 412 } else { 413 date = outcome.EnvelopeContent.SignerInfo.CertificateChain[0].NotAfter 414 } 415 } 416 } 417 418 if err != nil { 419 return author, date, false, err 420 } 421 422 // Verification Succeeded. 423 return author, date, true, nil 424 } 425 426 func CheckExpiryErr(verificationResults []*notation.ValidationResult, notAfter time.Time, err error) bool { 427 for _, result := range verificationResults { 428 if result.Type == trustpolicy.TypeExpiry { 429 if errors.Is(err, result.Error) { 430 return true 431 } 432 } else if result.Type == trustpolicy.TypeAuthenticTimestamp { 433 if errors.Is(err, result.Error) && time.Now().After(notAfter) { 434 return true 435 } else { 436 return false 437 } 438 } 439 } 440 441 return false 442 } 443 444 func UploadCertificate( 445 notationStorage certificateStorage, certificateContent []byte, truststoreType string, 446 ) error { 447 // validate truststore type 448 if !validateTruststoreType(truststoreType) { 449 return zerr.ErrInvalidTruststoreType 450 } 451 452 // validate certificate 453 if _, ok, err := parseAndValidateCertificateContent(certificateContent); !ok { 454 return err 455 } 456 457 // store certificate 458 err := notationStorage.StoreCertificate(certificateContent, truststoreType) 459 460 return err 461 } 462 463 func (local *CertificateLocalStorage) StoreCertificate( 464 certificateContent []byte, truststoreType string, 465 ) error { 466 // add certificate to "{rootDir}/_notation/truststore/x509/{type}/default/{name.crt}" 467 configDir, err := local.GetNotationDirPath() 468 if err != nil { 469 return err 470 } 471 472 name := godigest.FromBytes(certificateContent) 473 474 // store certificate 475 truststorePath := path.Join(configDir, dir.TrustStoreDir, "x509", truststoreType, truststoreName, name.String()) 476 477 if err := os.MkdirAll(filepath.Dir(truststorePath), defaultDirPerms); err != nil { 478 return err 479 } 480 481 return os.WriteFile(truststorePath, certificateContent, defaultFilePerms) 482 } 483 484 func (cloud *CertificateAWSStorage) StoreCertificate( 485 certificateContent []byte, truststoreType string, 486 ) error { 487 name := path.Join(truststoreType, truststoreName, godigest.FromBytes(certificateContent).Encoded()) 488 description := "notation certificate" 489 secret := base64.StdEncoding.EncodeToString(certificateContent) 490 secretInputParam := &secretsmanager.CreateSecretInput{ 491 Name: &name, 492 Description: &description, 493 SecretString: &secret, 494 } 495 496 _, err := cloud.secretsManagerClient.CreateSecret(context.Background(), secretInputParam) 497 498 if err != nil && IsResourceExistsException(err) { 499 return nil 500 } 501 502 return err 503 } 504 505 func validateTruststoreType(truststoreType string) bool { 506 for _, t := range truststore.Types { 507 if string(t) == truststoreType { 508 return true 509 } 510 } 511 512 return false 513 } 514 515 func validateTruststoreName(truststoreName string) bool { 516 if strings.Contains(truststoreName, "..") { 517 return false 518 } 519 520 return regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`).MatchString(truststoreName) 521 } 522 523 // implementation from https://github.com/notaryproject/notation-core-go/blob/main/x509/cert.go#L20 524 func parseAndValidateCertificateContent(certificateContent []byte) ([]*x509.Certificate, bool, error) { 525 var certs []*x509.Certificate 526 527 block, rest := pem.Decode(certificateContent) 528 if block == nil { 529 // data may be in DER format 530 derCerts, err := x509.ParseCertificates(certificateContent) 531 if err != nil { 532 return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err) 533 } 534 535 certs = append(certs, derCerts...) 536 } else { 537 // data is in PEM format 538 for block != nil { 539 cert, err := x509.ParseCertificate(block.Bytes) 540 if err != nil { 541 return []*x509.Certificate{}, false, fmt.Errorf("%w: %w", zerr.ErrInvalidCertificateContent, err) 542 } 543 certs = append(certs, cert) 544 block, rest = pem.Decode(rest) 545 } 546 } 547 548 if len(certs) == 0 { 549 return []*x509.Certificate{}, false, fmt.Errorf("%w: no valid certificates found in payload", 550 zerr.ErrInvalidCertificateContent) 551 } 552 553 return certs, true, nil 554 }