Othent / KeyManagementService

The Othent KMS package.
MIT License
0 stars 1 forks source link

Fix `signDataItem` signature verification. #23

Open Danziger opened 3 months ago

Danziger commented 3 months ago

Using signDataItem() and then calling dataItem.isValid() always returns false, so the following example will never submit the data item to a bundler:

import { DataItem } from "arbundles";
import { Othent } from "@othent/kms";

const othent = new Othent({ appInfo, throwErrors: false, ... });

// Make sure the user is authenticated, or prompt them to authenticate:
await othent.requireAuth();

// Sign the DataItem:
const signedDataItemBuffer = await othent.signDataItem({
    data: "This is an example data",
    tags: [{
        name: "Content-Type",
        value: "text/plain"
    }]
});

// Load the result into a DataItem instance:
const dataItem = new DataItem(signedDataItemBuffer);

// Verify the DataItem's signature:
const isValid = await dataItem.isValid();

if (isValid) {
    // Submit it to a bundler:
    await fetch(`https://node2.bundlr.network/tx`, {
        method: "POST",
        headers: {
            "Content-Type": "application/octet-stream"
        },
        body: dataItem.getRaw()
    });
}

This issue was already present in v1.

Danziger commented 2 months ago

New but failed attempt based on protocol.land:

  import base64url from 'base64url';

  // ...

  async signDataItem(dataItem: DataItem): Promise<ArrayBufferLike> {
    const { sub, publicKey } = await this.requireUserDataOrThrow();

    const { data, tags, ...options } = dataItem;

    const signFn = this.api.getSignerSignFn(sub);
    const { crypto } = this;

    class MySigner implements Signer {
      publicKey: Buffer;
      signatureType = 1;
      signatureLength = 512;
      ownerLength = 512;
      sign = signFn;

      constructor(publicKeyBuffer: Buffer) {
        this.publicKey = publicKeyBuffer;
      }

      static async verify(pk: string, message: Uint8Array, signature: Uint8Array): Promise<boolean> {
        // return await Arweave.crypto.verify(pk, message, signature);

        const publicJWK: JsonWebKey = {
          e: "AQAB",
          ext: true,
          kty: "RSA",
          n: pk,
        };

        const cryptoKey = await crypto.subtle.importKey(
          "jwk",
          publicJWK,
          {
            name: "RSA-PSS",
            hash: "SHA-256",
          },
          false,
          ["verify"],
        );

        return await crypto.subtle.verify({ name: "RSA-PSS" }, cryptoKey, signature, message);
      }
    };
    const signer = new MySigner(base64url.toBuffer(publicKey));

    /*
    const publicJWK: JWKInterface = {
      e: "AQAB",
      kty: "RSA",
      n: publicKey,
    };

    const signer = new ArweaveSigner(publicJWK);
    */

    const opts: DataItemCreateOptions = {
      ...options,
      tags: this.addCommonTags(tags),
    };

    const dataItemInstance = createData(data, signer, opts);

    // DataItem.sign() sets the DataItem's `id` property and returns its `rawId`:
    await dataItemInstance.sign(signer);

    return dataItemInstance.getRaw().buffer;
  }
Danziger commented 2 months ago

The error is thrown from

https://github.com/warp-contracts/warp-arbundles/blob/main/src/signing/chains/ArweaveSigner.ts

Which calls:

https://github.com/ArweaveTeam/arweave-js/blob/master/src/common/lib/crypto/webcrypto-driver.ts

Which ends up executing:

  private async jwkToPublicCryptoKey(
    publicJwk: JWKPublicInterface
  ): Promise<CryptoKey> {
    return this.driver.importKey(
      "jwk",
      publicJwk,
      {
        name: "RSA-PSS",
        hash: {
          name: "SHA-256",
        },
      },
      false,
      ["verify"]
    );
  }
Danziger commented 2 months ago

The problem seems to be that crypto.subtle.importKey expects the n property of the JWK object to be of type string, but warp-arbundles requires the publicKey property (same as n) to be of type Buffer.

The verify that, update the signDataItem()code to:

    const signer: Signer = {
      publicKey: toBuffer(publicKey),
      signatureType: 1,
      signatureLength: 512,
      ownerLength: 512,
      sign: this.api.getSignerSignFn(sub),
      // Note we don't provide `verify` as it's not used anyway:
      // verify: () => true,
    };

    const original = this.crypto.subtle.importKey.bind(this.crypto.subtle) as Function;

    this.crypto.subtle.importKey = (...args: any[]) => {
      console.log("ARGS =", args);

      return original(...args);
    }

    const publicJwk = {
      kty: "RSA",
      e: "AQAB",
      n: publicKey,
    } as const;

    this.crypto.subtle.importKey(
      "jwk",
      publicJwk,
      {
        name: "RSA-PSS",
        hash: {
          name: "SHA-256",
        },
      },
      false,
      ["verify"]
    );

You'll see two calls to importKeys in the console, the one in the example above and the one from warp-arbundles, which will throw an error. If we change n: publicKey to n: toBuffer(publicKey), that will also throw an error.

Danziger commented 2 months ago

This might be resolved if we migrate from warp-arbundles to https://github.com/DHA-Team/arbundles