internal/verifier/verifier.go (229 lines of code) (raw):

// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). You may // not use this file except in compliance with the License. A copy of the // License is located at // // http://aws.amazon.com/apache2.0 // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. // Package verifier verified provides functionality to verify signatures generated using AWS Signer // in accordance with the NotaryProject Plugin contract. package verifier import ( "context" "crypto/sha512" "crypto/x509" "fmt" "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws/arn" "github.com/aws/aws-sdk-go-v2/service/signer" "github.com/aws/aws-signer-notation-plugin/internal/client" "github.com/aws/aws-signer-notation-plugin/internal/logger" "github.com/aws/aws-signer-notation-plugin/internal/slices" "github.com/notaryproject/notation-plugin-framework-go/plugin" ) const ( wildcardIdentity = "*" errMsgWildcardIdentity = "The AWSSigner plugin does not support wildcard identity in the trust policy." attrSigningProfileVersion = "com.amazonaws.signer.signingProfileVersion" attrSigningJob = "com.amazonaws.signer.signingJob" signingSchemeAuthority = "notary.x509.signingAuthority" errMsgCertificateParse = "unable to parse certificates in certificate chain." errMsgAttributeParse = "unable to parse attribute %q." reasonTrustedIdentityFailure = "Signature publisher doesn't match any trusted identities." reasonTrustedIdentitySuccessFmt = "Signature publisher matched %q trusted identity." reasonNotRevoked = "Signature is not revoked." reasonRevokedResourceFmt = "Resource(s) %s have been revoked." reasonRevokedCertificate = "Certificate(s) have been revoked." platformNotation = "Notation-OCI-SHA384-ECDSA" ) var verificationCapabilities = []plugin.Capability{ plugin.CapabilityTrustedIdentityVerifier, plugin.CapabilityRevocationCheckVerifier} // Verifier verifies signature generated using AWS Signer. type Verifier struct { awssigner client.Interface } // New returns Verifier given an AWS Signer client. func New(s client.Interface) *Verifier { return &Verifier{awssigner: s} } // Verify provides extended verification (including trusted-identity and revocation check) // for signatures generated using AWS Signer. func (v *Verifier) Verify(ctx context.Context, request *plugin.VerifySignatureRequest) (*plugin.VerifySignatureResponse, error) { log := logger.GetLogger(ctx) log.Debug("validating VerifySignatureRequest") if err := validate(request); err != nil { log.Debugf("validate VerifySignatureRequest error :%s", err) return nil, err } response := plugin.VerifySignatureResponse{ VerificationResults: make(map[plugin.Capability]*plugin.VerificationResult), } if slices.Contains(request.TrustPolicy.SignatureVerification, plugin.CapabilityTrustedIdentityVerifier) { log.Debug("validating trusted identity") if err := validateTrustedIdentity(request, &response); err != nil { log.Debugf("validate trusted identity error :%v", err) return nil, err } log.Debugf("verification response: %+v\n", response) } if slices.Contains(request.TrustPolicy.SignatureVerification, plugin.CapabilityRevocationCheckVerifier) { log.Debug("validating revocation status") if err := v.validateRevocation(ctx, request, &response); err != nil { log.Debugf("validate revocation status error :%v", err) return nil, err } log.Debugf("verification response: %+v\n", response) } // marking both signing-job ARN and signing-profile-version arn as processed attributes here because the plugin should // return both of them as processed even if the revocation call was skipped response.ProcessedAttributes = slices.AppendIfNotPresent(response.ProcessedAttributes, attrSigningProfileVersion) response.ProcessedAttributes = slices.AppendIfNotPresent(response.ProcessedAttributes, attrSigningJob) return &response, nil } func validate(req *plugin.VerifySignatureRequest) error { if req.ContractVersion != plugin.ContractVersion { return plugin.NewUnsupportedContractVersionError(req.ContractVersion) } if slices.Contains(req.TrustPolicy.TrustedIdentities, wildcardIdentity) { return plugin.NewValidationError(errMsgWildcardIdentity) } for _, value := range req.TrustPolicy.SignatureVerification { if !pluginCapabilitySupported(value) { return plugin.NewValidationErrorf("'%s' is not a supported plugin capability", value) } } critcAttr := req.Signature.CriticalAttributes if critcAttr.AuthenticSigningTime.IsZero() { return plugin.NewValidationError("missing authenticSigningTime") } if !strings.EqualFold(critcAttr.SigningScheme, signingSchemeAuthority) { return plugin.NewUnsupportedError(fmt.Sprintf("'%s' signing scheme", req.Signature.CriticalAttributes.SigningScheme)) } return nil } func validateTrustedIdentity(request *plugin.VerifySignatureRequest, response *plugin.VerifySignatureResponse) error { signatureIdentity, err := getValueAsString(request.Signature.CriticalAttributes.ExtendedAttributes, attrSigningProfileVersion) if err != nil { return err } var trustedArns []string for _, identity := range request.TrustPolicy.TrustedIdentities { if _, ok := isSigningProfileArn(identity); ok { trustedArns = append(trustedArns, identity) } } result := &plugin.VerificationResult{ Success: false, Reason: reasonTrustedIdentityFailure, } var profileMatch bool for _, identity := range request.TrustPolicy.TrustedIdentities { if arn, ok := isSigningProfileArn(identity); ok { s := strings.Split(arn.Resource, "/") if len(s) == 3 { // if profile arn lastIndex := strings.LastIndex(signatureIdentity, "/") if lastIndex != -1 && strings.EqualFold(signatureIdentity[:lastIndex], identity) { profileMatch = true } } else if len(s) == 4 { // if profile version arn if strings.EqualFold(signatureIdentity, identity) { profileMatch = true } } if profileMatch { result.Success = true result.Reason = fmt.Sprintf(reasonTrustedIdentitySuccessFmt, identity) break } } } response.VerificationResults[plugin.CapabilityTrustedIdentityVerifier] = result return nil } func isSigningProfileArn(s string) (arn.ARN, bool) { if a, err := arn.Parse(s); err == nil { return a, a.Service == "signer" && strings.HasPrefix(a.Resource, "/signing-profiles/") } return arn.ARN{}, false } func getValueAsString(m map[string]interface{}, k string) (string, error) { if val, ok := m[k]; ok { if s, ok := val.(string); ok { return s, nil } } return "", plugin.NewValidationErrorf(errMsgAttributeParse, k) } func (v *Verifier) validateRevocation(ctx context.Context, request *plugin.VerifySignatureRequest, response *plugin.VerifySignatureResponse) error { profileVersionArn, err := getValueAsString(request.Signature.CriticalAttributes.ExtendedAttributes, attrSigningProfileVersion) if err != nil { return err } jobArn, err := getValueAsString(request.Signature.CriticalAttributes.ExtendedAttributes, attrSigningJob) if err != nil { return err } certHashes, err := hashCertificates(request.Signature.CertificateChain) if err != nil { return plugin.NewValidationError(errMsgCertificateParse) } input := &signer.GetRevocationStatusInput{ CertificateHashes: certHashes, JobArn: aws.String(jobArn), PlatformId: aws.String(platformNotation), ProfileVersionArn: aws.String(profileVersionArn), SignatureTimestamp: request.Signature.CriticalAttributes.AuthenticSigningTime, } result := &plugin.VerificationResult{ Success: true, Reason: reasonNotRevoked, } output, err := v.awssigner.GetRevocationStatus(ctx, input) if err != nil { result.Success = false result.Reason = fmt.Sprintf("GetRevocationStatus call failed with error: %+v", err) } else { if len(output.RevokedEntities) > 0 { result.Success = false result.Reason = getRevocationResultReason(output.RevokedEntities) } } response.VerificationResults[plugin.CapabilityRevocationCheckVerifier] = result return nil } func getRevocationResultReason(revokedEntities []string) string { var resources string var certRevoked bool for _, resource := range revokedEntities { if strings.HasPrefix(resource, "arn") { if resources == "" { resources += resource } else { resources = resources + ", " + resource } } else { certRevoked = true } } var reason string if resources != "" { reason = fmt.Sprintf(reasonRevokedResourceFmt, resources) } if certRevoked { reason += reasonRevokedCertificate } return reason } func hashCertificates(certStrings [][]byte) ([]string, error) { var certHashes []string for _, certString := range certStrings { // notation always passes cert in DER format cert, err := x509.ParseCertificate(certString) if err != nil { return nil, err } certHashes = append(certHashes, hashCertificate(*cert)) } for i := range certHashes { if i == len(certHashes)-1 { certHashes[i] = certHashes[i] + certHashes[i] } else { certHashes[i] = certHashes[i] + certHashes[i+1] } } return certHashes, nil } func hashCertificate(cert x509.Certificate) string { h := sha512.New384() h.Write(cert.RawTBSCertificate) return fmt.Sprintf("%x", h.Sum(nil)) } func pluginCapabilitySupported(capability plugin.Capability) bool { return slices.Contains(verificationCapabilities, capability) }