Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CodeArtifact .publishPackageVersion fails for large generic artifact with 'InvalidSignatureException' #1217

Closed
cloudshiftchris opened this issue Feb 15, 2024 · 7 comments · Fixed by #1224
Labels
bug This issue is a bug. needs-triage This issue or PR still needs to be triaged.

Comments

@cloudshiftchris
Copy link

cloudshiftchris commented Feb 15, 2024

Describe the bug

Uploading a large (937MB) file to AWS CodeArtifact as a generic artifact fails with this exception:

x-amzn-errortype: InvalidSignatureException:http://internal.amazon.com/coral/com.amazon.coral.service/
x-amzn-requestid: 9610da2f-c4ca-4b81-b59d-cd2964dc9fe7

The SHA256 has been validated as correctly calculated.

Uploading smaller files works (have not tested for size boundaries).
Forcing a SHA256 mismatch results in a different, specific exception.

Furthermore, perhaps related (or not), the same attempt using AWS CLI fails for different reasons (SSL/EOF errors - seems like CodeArtifact terminates the connection; there are no network proxies or otherwise involved).

937MB is within the 5GB limit for an individual CodeArtifact asset.

AWS Kotlin SDK 1.0.56

Expected behavior

When invoked with valid data AWS SDK correctly uploads large files into CodeArtifact.

Current behavior

Fails with this exception:

09:24:10.461 [DefaultDispatcher-worker-3] DEBUG httpTraceMiddleware - HttpResponse:
HTTP 403: Forbidden
content-length: 192
content-type: application/json
date: Thu, 15 Feb 2024 17:20:33 GMT
x-amzn-errortype: InvalidSignatureException:http://internal.amazon.com/coral/com.amazon.coral.service/
x-amzn-requestid: 9610da2f-c4ca-4b81-b59d-cd2964dc9fe7


09:24:10.463 [DefaultDispatcher-worker-3] TRACE aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine - response body available
09:24:10.463 [DefaultDispatcher-worker-3] TRACE aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine - response body finished: bytesConsumed=192
09:24:10.463 [DefaultDispatcher-worker-3] TRACE aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine - connection released: conn(id=1055222267)=Connection{codeartifact.us-east-1.amazonaws.com:443, proxy=DIRECT hostAddress=codeartifact.us-east-1.amazonaws.com/34.196.74.171:443 cipherSuite=TLS_AES_128_GCM_SHA256 protocol=h2}; connPool: total=1, idle=1
09:24:10.463 [DefaultDispatcher-worker-3] TRACE aws.smithy.kotlin.runtime.http.engine.okhttp.OkHttpEngine - call complete
09:24:10.467 [DefaultDispatcher-worker-3] DEBUG aws.smithy.kotlin.runtime.http.middleware.RetryMiddleware - request failed with non-retryable error
09:24:10.467 [DefaultDispatcher-worker-3] TRACE aws.smithy.kotlin.runtime.http.operation.OperationHandler - operation failed
aws.sdk.kotlin.services.codeartifact.model.CodeartifactException: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
	at aws.sdk.kotlin.services.codeartifact.serde.PublishPackageVersionOperationDeserializerKt.throwPublishPackageVersionError(PublishPackageVersionOperationDeserializer.kt:72) ~[codeartifact-jvm-1.0.56.jar:?]
	at aws.sdk.kotlin.services.codeartifact.serde.PublishPackageVersionOperationDeserializerKt.access$throwPublishPackageVersionError(PublishPackageVersionOperationDeserializer.kt:1) ~[codeartifact-jvm-1.0.56.jar:?]
	at aws.sdk.kotlin.services.codeartifact.serde.PublishPackageVersionOperationDeserializerKt$throwPublishPackageVersionError$1.invokeSuspend(PublishPackageVersionOperationDeserializer.kt) ~[codeartifact-jvm-1.0.56.jar:?]
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33) [kotlin-stdlib-1.9.22.jar:1.9.22-release-704]
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:283) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith$default(DispatchedContinuation.kt:278) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.DispatchedCoroutine.afterResume(Builders.common.kt:261) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:102) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46) [kotlin-stdlib-1.9.22.jar:1.9.22-release-704]
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684) [kotlinx-coroutines-core-jvm-1.7.3.jar:?]
Exception in thread "main" aws.sdk.kotlin.services.codeartifact.model.CodeartifactException: The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
	at aws.sdk.kotlin.services.codeartifact.serde.PublishPackageVersionOperationDeserializerKt.throwPublishPackageVersionError(PublishPackageVersionOperationDeserializer.kt:72)
	at aws.sdk.kotlin.services.codeartifact.serde.PublishPackageVersionOperationDeserializerKt.access$throwPublishPackageVersionError(PublishPackageVersionOperationDeserializer.kt:1)
	at aws.sdk.kotlin.services.codeartifact.serde.PublishPackageVersionOperationDeserializerKt$throwPublishPackageVersionError$1.invokeSuspend(PublishPackageVersionOperationDeserializer.kt)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:283)
	at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith$default(DispatchedContinuation.kt:278)
	at kotlinx.coroutines.DispatchedCoroutine.afterResume(Builders.common.kt:261)
	at kotlinx.coroutines.AbstractCoroutine.resumeWith(AbstractCoroutine.kt:102)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:46)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
	at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
	at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
	at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
	at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)

Steps to Reproduce

The code below can be used to reproduce the issue; in the comments are the command-line equivalents.

suspend fun main() {

     Configurator.setRootLevel(Level.TRACE);

    // aws codeartifact publish-package-version --debug --profile cloudshift-lab1 --region us-east-1 --domain repro-test-domain --domain-owner 331062209604 --repository generic --format generic --namespace whatever --package this.is.the.package --package-version 1.0.0 --asset-sha256 f2e321d69a55ce94bb472bc2c8e83b8271d7775a7ba61b690e1d2a654b6af4f1 --asset-name the.asset.name --asset-content build/sql-database-fingerprint-baseline.bak.zip

    // aws codeartifact publish-package-version --debug --profile cloudshift-lab1 --region us-east-1 --domain repro-test-domain --domain-owner 331062209604 --repository generic --format generic --namespace whatever --package this.is.the.package2 --package-version 1.0.0 --asset-sha256 a8451eeda314d0568b5340498b36edf147a8f0d692c5ff58082d477abe9146e4 --asset-name the.asset.name --asset-content gradle/wrapper/gradle-wrapper.jar

    /*
    An error occurred (ValidationException) when calling the PublishPackageVersion operation: The provided content hashes do not match the calculated content hashes. Hash algorithm 'SHA-256', Expected hash 'f2e321d69a55ce94bb472bc2c8e83b8271d7775a7ba61b690e1d2a654b6af4f1', Actual hash 'a8451eeda314d0568b5340498b36edf147a8f0d692c5ff58082d477abe9146e4'


     */


    /**
     * Use a valid profile (or other credentials provider) that has at least these permissions:
     * ``` {
     *             "Action": "codeartifact:Publish*",
     *             "Resource": "*",
     *             "Effect": "Allow"
     *         },
     * ```
     */
    val creds = ProfileCredentialsProvider("cloudshift-lab1")

    val client = CodeartifactClient {
        credentialsProvider = creds
        region = "us-east-1"
        logMode = LogMode.LogRequest + LogMode.LogResponse
    }

    val testFile = File("build/sql-database-fingerprint-baseline.bak.zip")

    // https://repro-test-domain-331062209604.d.codeartifact.us-east-1.amazonaws.com/generic/generic/
    client.publishPackageVersion {
        domain = "repro-test-domain"
        domainOwner = "331062209604"
        repository = "generic"
        namespace = "whatever"
        format = PackageFormat.Generic
        `package` = "this.is.the.package"
        packageVersion = "1.0.0"
        assetSha256 = testFile.sha256()
        assetName = "the.asset.name"
        assetContent = testFile.asByteStream()
        unfinished = false
    }
}

@OptIn(ExperimentalStdlibApi::class)
public fun File.sha256(): String {
    val digest = MessageDigest.getInstance("SHA-256")
    val buffer = ByteArray(8192)
    DigestInputStream(inputStream(), digest).use {
        while(it.read(buffer) != -1) {}
    }
    return digest.digest().toHexString()
}

Possible Solution

Have not identified a working means to upload large assets to CodeArtifact.

Context

Blocked; unable to upload large assets to CodeArtifact

Originally discovered from this code in a CodeArtifact Gradle Plugin.

AWS Kotlin SDK version used

1.0.56

Platform (JVM/JS/Native)

JVM

Operating System and version

macOS Sonoma

@cloudshiftchris cloudshiftchris added bug This issue is a bug. needs-triage This issue or PR still needs to be triaged. labels Feb 15, 2024
@cloudshiftchris
Copy link
Author

fail-kotlin-sdk.txt

Full debug log

@cloudshiftchris
Copy link
Author

fail-awscli.txt

AWS CLI debug logs

@cloudshiftchris
Copy link
Author

In case this is a service issue, the AWS case # is 170802144901259

@cloudshiftchris
Copy link
Author

cloudshiftchris commented Feb 16, 2024

The signature algorithm appears to be incorrect for Kotlin SDK, when compared to the same request for Java SDK v2.

Java SDK sends the payload as a single chunk, whereas Kotlin SDK uses was-chunk encoding.

From the documented signature algorithms (single chunks, multiple chunks) the value of x-amz-content-sha256 is different:

  • for single chunk its the calculated sha256 of the entire payload, matching what is passed to this CodeArtifact call;
  • for multiple chunks its the constant STREAMING-AWS4-HMAC-SHA256-PAYLOAD

For the Java SDK call it correctly provides the x-amz-content-sha256 header and signature; for the Kotlin SDK call it provides x-amz-content-sha256:f2e321d69a55ce94bb472bc2c8e83b8271d7775a7ba61b690e1d2a654b6af4f1 instead of x-amz-content-sha256:STREAMING-AWS4-HMAC-SHA256-PAYLOAD, in both the canonical request and request headers.

Java canonical request:
    POST
    /v1/package/version/publish
    asset=the.asset.name&domain=repro-test-domain&domain-owner=331062209604&format=generic&namespace=whatever&package=this.is.the.package2&repository=generic&version=1.0.0
    amz-sdk-invocation-id:56ffa762-0f24-34c1-f3c5-37b5da6be7b2
    amz-sdk-request:attempt=1; max=4
    content-length:937099048
    content-type:application/octet-stream
    host:codeartifact.us-east-1.amazonaws.com
    x-amz-content-sha256:f2e321d69a55ce94bb472bc2c8e83b8271d7775a7ba61b690e1d2a654b6af4f1
    x-amz-date:20240216T035949Z
    x-amz-security-token:<redacted>

    amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-security-token
    f2e321d69a55ce94bb472bc2c8e83b8271d7775a7ba61b690e1d2a654b6af4f1

Kotlin canonical request
    POST
    /v1/package/version/publish
    asset=the.asset.name&domain=repro-test-domain&domain-owner=331062209604&format=generic&namespace=whatever&package=this.is.the.package&repository=generic&unfinished=false&version=1.0.0
    amz-sdk-invocation-id:b3fa77eb-4953-4eae-abf6-bd36f57a3745
    amz-sdk-request:attempt=1; max=3
    content-encoding:aws-chunked
    content-type:application/octet-stream
    host:codeartifact.us-east-1.amazonaws.com
    transfer-encoding:chunked
    x-amz-content-sha256:f2e321d69a55ce94bb472bc2c8e83b8271d7775a7ba61b690e1d2a654b6af4f1
    x-amz-date:20240215T172033Z
    x-amz-decoded-content-length:937099048
    x-amz-security-token:<redacted>
    x-amz-user-agent:aws-sdk-kotlin/1.0.56

    amz-sdk-invocation-id;amz-sdk-request;content-encoding;content-type;host;transfer-encoding;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-security-token;x-amz-user-agent
    STREAMING-AWS4-HMAC-SHA256-PAYLOAD

...this results in the CodeArtifact service calculating a different signature than what as incorrectly calculated by the Kotlin SDK client.

Using a chunked transfer for this CodeArtifact call also raises the question of where does the asset-sha256 value go (it's part of the SDK parameters for this call)? Is it then unused, or ... ? Likely a better question for the service-side of this, what is expected/supported (i.e. is was-chunked even supported for this API call)?

Answering my own question... from the API docs this call looks to require single-chunk transfer:

POST /v1/package/version/publish?asset=assetName&domain=domain&domain-owner=domainOwner&format=format&namespace=namespace&package=package&repository=repository&unfinished=unfinished&version=packageVersion HTTP/1.1
x-amz-content-sha256: assetSHA256

assetContent

The Kotlin SDK code is partially consistent with that API spec:

        builder.headers {
            if (input.assetSha256?.isNotEmpty() == true) append("x-amz-content-sha256", input.assetSha256)
        }

...however, manually setting the x-amz-content-sha256 header is incorrect wrt multi-chunk encoding.

Omitting the assetSha256 parameter removes the x-amz-content-sha256 header entirely, which is also incorrect wrt multi-chunk encoding (this also fails with the same signature error).

So it appears the issue here is the Kotlin SDK, for this API call, is selecting multi-chunk encoding which isn't compatible with the spec for this call.

Likely fixable in aws.smithy.kotlin.runtime.http.auth.AwsHttpSigner#sign - before checking for body.isEligibleForAwsChunkedStreaming check to see if x-amz-content-sha256 is already set, if so then return a HashSpecification.HashLiteral for it. Perhaps with a pre-send check to ensure that x-amz-content-sha256 is set appropriately vis-a-vis content-encoding.

@lauzadis
Copy link
Member

lauzadis commented Feb 16, 2024

Hi, thanks for the detailed report, I've reproduced the issue and am working on a fix. In the meantime, here is a temporary workaround using an interceptor:

class PrecomputedHashInterceptor(val hash: String): HttpInterceptor {
    override suspend fun modifyBeforeRetryLoop(context: ProtocolRequestInterceptorContext<Any, HttpRequest>): HttpRequest {
        context.executionContext[AwsSigningAttributes.HashSpecification] = HashSpecification.Precalculated(hash)
        return context.protocolRequest
    }
}

val testFile = File("build/sql-database-fingerprint-baseline.bak.zip")
val testFileSha256 = testFile.sha256()

val client = CodeartifactClient {
    credentialsProvider = creds
    region = "us-east-1"
    logMode = LogMode.LogRequest + LogMode.LogResponse
}

client.withConfig { 
    interceptors += PrecomputedHashInterceptor(testFileSha256)
}.use { precomputedHashClient ->
    precomputedHashClient.publishPackageVersion {
        ...
        assetSha256 = testFileSha256
    }
}

Copy link

⚠️COMMENT VISIBILITY WARNING⚠️

Comments on closed issues are hard for our team to see.
If you need more assistance, please either tag a team member or open a new issue that references this one.
If you wish to keep having a conversation with other community members under this issue feel free to do so.

@lauzadis
Copy link
Member

lauzadis commented Feb 19, 2024

Hi, I merged a fix for this. It should be available in tomorrow's release, v1.0.61. Thanks!

Besides upgrading, there is no action needed from you to receive the fix. The change disables aws-chunked content encoding for all services except S3, since that's the only service that explicitly supports it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug This issue is a bug. needs-triage This issue or PR still needs to be triaged.
Projects
None yet
2 participants