Skip to content

google-kms signer: hashAlgorithm SHA3_256 is ignored for P-256 keys, signature computed over SHA2_256 digest #2367

Open

Description

@vishalchangrani

Summary

Flow CLI's google-kms signer produces signatures that fail on-chain verification when the account key is registered with hashAlgorithm: SHA3_256, even when the Flow CLI account config in flow.json
declares "hashAlgorithm": "SHA3_256". The same signer, same GCP KMS key, same transaction template works correctly when the on-chain key is registered with SHA2_256.

Root cause appears to be that the google-kms signer always hashes the transaction envelope with SHA2_256 (because the underlying GCP KMS asymmetric-sign API ties the algorithm name EC_SIGN_P256_SHA256
to a SHA256 digest in the request shape), regardless of the hashAlgorithm field in the flow.json account config. When the on-chain account key is registered with SHA3_256, Flow's verifier hashes the
envelope with SHA3_256, which doesn't match the SHA2_256 digest the signer actually signed → signature is not valid.

Reproduction on mainnet (verifiable on the block explorer)

Two mainnet accounts share the same P-256 pubkey (backed by the same GCP KMS P-256 key, algorithm EC_SIGN_P256_SHA256) registered with different hashAlgorithm values:

  • Account A — 0xad2b341394a2e19a — Key 0: ECDSA_P256, SHA3_256, weight 1000, pubkey 755a4234...314a5d7a44ce
  • Account B — 0x6dec6e64a13b881e — Key 0: ECDSA_P256, SHA2_256, weight 1000, pubkey (same as A) 755a4234...314a5d7a44ce

Both accounts' key layouts are observable via flow accounts get --include keys --network mainnet.

flow.json for the reproduction differs only in hashAlgorithm:

  {                                                                                                                                                                                                        
    "accounts": {                                                                                                                                                                                            
      "acct-a": {                                                                                                                                                                                          
        "address": "0xad2b341394a2e19a",                                                                                                                                                                     
        "key": { "type": "google-kms", "index": 0, "signatureAlgorithm": "ECDSA_P256",                                                                                                                     
                 "hashAlgorithm": "SHA3_256", "resourceID": "projects/.../cryptoKeyVersions/1" }                                                                                                             
      },                                                                                                                                                                                                     
      "acct-b": {                                                                                                                                                                                            
        "address": "0x6dec6e64a13b881e",                                                                                                                                                                     
        "key": { "type": "google-kms", "index": 0, "signatureAlgorithm": "ECDSA_P256",                                                                                                                       
                 "hashAlgorithm": "SHA2_256", "resourceID": "projects/.../cryptoKeyVersions/1" }                                                                                                             
      }                                                                                                                                                                                                      
    }                                                                                                                                                                                                        
  }                                                                                                                                                                                                          

Account B (SHA2_256) — WORKS

Tx 0x07a0c641e4ccac13165ae6a0168be819f712ea650c8c5e1506d8b9c75633c488 sealed cleanly, flow.AccountKeyAdded emitted, verified on-chain.
https://www.flowscan.io/tx/07a0c641e4ccac13165ae6a0168be819f712ea650c8c5e1506d8b9c75633c488

Account A (SHA3_256) — FAILS

Tx 0x3e687ff5b46f729d4f622657fa719310927249e0bdefd532b72785ae99ac9369 sealed but failed verification:

  Status  SEALED                                                                                                                                                                                             
  Transaction Error:                                                                                                                                                                                         
  [Error Code: 1009] ... transaction verification failed:                                                                                                                                                  
  [Error Code: 1006] invalid proposal key: public key 0 on account ad2b341394a2e19a does not have a valid signature:                                                                                         
  [Error Code: 1009] invalid envelope key: public key 0 on account ad2b341394a2e19a does not have a valid signature:                                                                                       
  signature is not valid                                                                                                                                                                                     

https://www.flowscan.io/tx/3e687ff5b46f729d4f622657fa719310927249e0bdefd532b72785ae99ac9369

Emulator reproduction without mainnet access: create a test account carrying a P-256 / SHA3_256 key backed by a mocked GCP KMS stub. Same behavior should reproduce 1-for-1.

Expected behavior

For flow transactions send ... with "type": "google-kms" and "hashAlgorithm": "SHA3_256":

  1. Flow CLI builds the tx envelope.
  2. Flow CLI computes the SHA3_256 digest of the envelope (not SHA2_256).
  3. Flow CLI calls GCP KMS AsymmetricSign with that 32-byte digest.
  4. GCP KMS signs the bytes it was given — it does not re-hash. See GCP asymmetric signing algorithms doc — client is
    responsible for hashing. The _SHA256 in EC_SIGN_P256_SHA256 is a request-shape type marker, not a server-side step.
  5. Flow CLI attaches the signature; Flow verifies the envelope using SHA3_256 (per the on-chain registration), matches; passes.

Actual behavior

Step 2 appears hardcoded to SHA2_256 regardless of flow.json's hashAlgorithm, producing a signature over the wrong digest. Flow verification (step 5, which correctly uses SHA3_256 per the on-chain
registration) rejects.

Why there's no user-side workaround

The hashAlgorithm in flow.json must match the on-chain registration (Flow's verifier uses the on-chain value, not the submitter's claim). Lying about it in flow.json still fails. The signer has to
compute the correct digest.

Likely fix location

google-kms signer in Flow CLI resolves flow.json's signer type and delegates to the GCP KMS integration in flow-go-sdk's crypto/cloudkms package (or thereabouts).

Pseudo-fix:

  // Current (likely): hasher pinned to SHA2_256 because the KMS algorithm name contains "SHA256".
  digest := sha256.Sum256(envelope.Bytes())                                                                                                                                                                  
  sig, err := kmsClient.AsymmetricSign(ctx, keyName, digest[:])                                                                                                                                            
                                                                                                                                                                                                             
  // Fix: honor the hashAlgorithm declared on the account key.                                                                                                                                             
  hasher, err := crypto.NewHasher(accountKey.HashAlgorithm)  // SHA2_256 or SHA3_256                                                                                                                         
  if err != nil { return nil, err }                                                                                                                                                                          
  digest := hasher.ComputeHash(envelope.Bytes())                                                                                                                                                             
  sig, err := kmsClient.AsymmetricSign(ctx, keyName, digest)                                                                                                                                                 

GCP's AsymmetricSignRequest has a Digest oneof with sha256/sha384/sha512. For an EC_SIGN_P256_SHA256 key, only sha256 is accepted at request-shape — but the 32 bytes can be any digest. GCP does
not re-hash.

If there's an upfront validation rejecting SHA3_256 for EC_SIGN_P256_SHA256 keys, it should be lifted: the on-chain registration is the source of truth, not the KMS algorithm name.

Suggested regression test

Emulator integration test:

  1. Create an emulator account with P-256 pubkey registered hashAlgorithm: SHA3_256, weight 1000.
  2. Config google-kms signer with a backing stub that returns a real ECDSA signature over whatever digest it receives.
  3. Sign a no-op transaction (transaction { prepare(signer: &Account) {} }); submit; assert SEALED without transaction verification failed.
  4. Sibling account with hashAlgorithm: SHA2_256 as control — both should pass.

Impact

Any Cadence account with a P-256 key registered as SHA3_256, backed by GCP KMS, cannot be signed for by Flow CLI's google-kms signer. The sibling secp256k1 / SHA2_256 case works. aws-kms signers not
affected (round-trip tested). Bug is specific to the P-256 + SHA3_256 corner of google-kms signing.

Priority

Medium — correctness bug in a corner of signer support, blocks a specific use case completely with no user-side workaround.

Activity

  1. added
    BugThe issue represents a bug, malfunction, regression
    on Nov 7, 2026
  2. vishalchangrani commented on Nov 7, 2026

    @vishalchangrani
    ContributorAuthor

    Workaround confirmed working end-to-end

    Wrote a small Python-side workaround that bypasses the google-kms signer and submits a SHA3_256-signed transaction directly. It confirms the diagnosis and gives a working path until the fix ships:

    1. Build the transaction via flow_py_sdk.
    2. Hash (domain_tag || envelope_bytes) with SHA3_256 locally.
    3. Call KeyManagementServiceClient.asymmetric_sign on the GCP KMS key, passing the 32-byte SHA3_256 digest in the request's digest.sha256 field — GCP KMS does not re-hash; it signs the 32 bytes
      as-is. The _SHA256 in EC_SIGN_P256_SHA256 is a request-shape type marker.
    4. Convert the DER signature from GCP to raw r || s.
    5. Submit via the gRPC Access API client.

    Mainnet verification: tx 0xaef3e3fefcef7619567195e0420970f9b7b30f412237e708b9af3584e5e42d5a sealed cleanly and
    verified on the SHA3_256 account from the original issue (0xad2b341394a2e19a). Immediately before that, the same tx template through the standard flow transactions send path with the google-kms signer
    failed with the same signature is not valid error from the issue body — confirming SHA2_256 is the only thing being hashed.

    Core custom-signer logic (Python)

    class SHA3P256KMSSigner(Signer):
        def __init__(self, resource_id: str):
            super().__init__()
            self._resource_id = resource_id
            self._kms = kms_v1.KeyManagementServiceClient()
    
        def sign(self, message: bytes, tag: bytes = None) -> bytes:
            prefix = tag if tag else b""
            digest = hashlib.sha3_256(prefix + message).digest()   # <-- the fix
            resp = self._kms.asymmetric_sign(
                request={"name": self._resource_id, "digest": {"sha256": digest}}
            )
            r, s = decode_dss_signature(resp.signature)            # DER -> raw r||s
            return r.to_bytes(32, "big") + s.to_bytes(32, "big")
    
    That ~10 lines, dropped into a custom Signer subclass, produces valid signatures against SHA3_256-registered P-256 keys. The symmetric fix in the Flow CLI / flow-go-sdk's crypto/cloudkms signer should look
    identical in shape — just honor the account key's declared hashAlgorithm when selecting the hasher.
    
    Suggested minimum regression fixture
    
    With the custom signer working against mainnet (and standard flow CLI not), an emulator fixture becomes straightforward: a test account with a P-256 pubkey registered as hashAlgorithm: SHA3_256 + a
    google-kms signer config. A no-op tx should seal cleanly after the fix; still error with signature is not valid before. Same fixture validates the fix for anyone implementing it.
    
    Happy to share the full workaround script if useful — just a bit of shaping around the custom Signer class above.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugThe issue represents a bug, malfunction, regression

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions