gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] branch master updated: wallet: support both protocol


From: gnunet
Subject: [taler-wallet-core] branch master updated: wallet: support both protocol versions
Date: Sat, 27 Nov 2021 20:57:10 +0100

This is an automated email from the git hooks/post-receive script.

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 5c4c2551 wallet: support both protocol versions
5c4c2551 is described below

commit 5c4c25516df9d65d29dc7f3f38b5a2a1a8e9e374
Author: Florian Dold <florian@dold.me>
AuthorDate: Sat Nov 27 20:56:58 2021 +0100

    wallet: support both protocol versions
---
 packages/taler-util/src/codec.ts                   |  23 ++
 packages/taler-util/src/libtool-version.ts         |   7 +-
 packages/taler-util/src/logging.ts                 |   3 +-
 packages/taler-util/src/talerCrypto.ts             |  27 ++-
 packages/taler-util/src/talerTypes.ts              | 111 ++++++++--
 packages/taler-util/src/walletTypes.ts             |   2 +
 packages/taler-wallet-cli/src/harness/harness.ts   |  24 +-
 packages/taler-wallet-cli/src/index.ts             |   7 +
 packages/taler-wallet-core/src/common.ts           |   5 +-
 .../src/crypto/workers/cryptoApi.ts                |   2 +
 .../src/crypto/workers/cryptoImplementation.ts     | 245 ++++++++++++++-------
 packages/taler-wallet-core/src/db.ts               |   5 +-
 .../src/operations/backup/import.ts                |   6 +-
 .../src/operations/backup/index.ts                 |  30 ++-
 .../taler-wallet-core/src/operations/deposits.ts   |  74 +++++--
 .../taler-wallet-core/src/operations/exchanges.ts  |  62 ++++--
 .../taler-wallet-core/src/operations/merchants.ts  |  14 +-
 packages/taler-wallet-core/src/operations/pay.ts   |  70 +++++-
 .../taler-wallet-core/src/operations/refresh.ts    |  48 ++--
 packages/taler-wallet-core/src/operations/tip.ts   |  14 +-
 .../taler-wallet-core/src/operations/withdraw.ts   |  43 +++-
 .../taler-wallet-core/src/util/coinSelection.ts    |  22 +-
 packages/taler-wallet-core/src/versions.ts         |   6 +-
 packages/taler-wallet-core/src/wallet.ts           |   4 +-
 24 files changed, 623 insertions(+), 231 deletions(-)

diff --git a/packages/taler-util/src/codec.ts b/packages/taler-util/src/codec.ts
index 8605ff33..2ea64a24 100644
--- a/packages/taler-util/src/codec.ts
+++ b/packages/taler-util/src/codec.ts
@@ -417,3 +417,26 @@ export function codecOptional<V>(innerCodec: Codec<V>): 
Codec<V | undefined> {
     },
   };
 }
+
+export type CodecType<T> = T extends Codec<infer X> ? X : any;
+
+export function codecForEither<T extends Array<Codec<unknown>>>(
+  ...alts: [...T]
+): Codec<CodecType<T[number]>> {
+  return {
+    decode(x: any, c?: Context): any {
+      for (const alt of alts) {
+        try {
+          return alt.decode(x, c);
+        } catch (e) {
+          continue;
+        }
+      }
+      throw new DecodingError(
+        `No alternative matched at at ${renderContext(c)}`,
+      );
+    },
+  };
+}
+
+const x = codecForEither(codecForString(), codecForNumber());
diff --git a/packages/taler-util/src/libtool-version.ts 
b/packages/taler-util/src/libtool-version.ts
index 17d2bbbd..ed11a4e9 100644
--- a/packages/taler-util/src/libtool-version.ts
+++ b/packages/taler-util/src/libtool-version.ts
@@ -27,14 +27,15 @@ export interface VersionMatchResult {
    * Is the first version compatible with the second?
    */
   compatible: boolean;
+
   /**
-   * Is the first version older (-1), newser (+1) or
+   * Is the first version older (-1), newer (+1) or
    * identical (0)?
    */
   currentCmp: number;
 }
 
-interface Version {
+export interface Version {
   current: number;
   revision: number;
   age: number;
@@ -64,7 +65,7 @@ export namespace LibtoolVersion {
     return { compatible, currentCmp };
   }
 
-  function parseVersion(v: string): Version | undefined {
+  export function parseVersion(v: string): Version | undefined {
     const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
     if (rest.length !== 0) {
       return undefined;
diff --git a/packages/taler-util/src/logging.ts 
b/packages/taler-util/src/logging.ts
index 8b9de1ab..117664d8 100644
--- a/packages/taler-util/src/logging.ts
+++ b/packages/taler-util/src/logging.ts
@@ -55,7 +55,7 @@ export function setGlobalLogLevelFromString(logLevelStr: 
string) {
       break;
     default:
       if (isNode) {
-        process.stderr.write(`Invalid log level, defaulting to WARNING`);
+        process.stderr.write(`Invalid log level, defaulting to WARNING\n`);
       } else {
         console.warn(`Invalid log level, defaulting to WARNING`);
       }
@@ -143,6 +143,7 @@ export class Logger {
       case LogLevel.Info:
       case LogLevel.Warn:
       case LogLevel.Error:
+        return true;
       case LogLevel.None:
         return false;
     }
diff --git a/packages/taler-util/src/talerCrypto.ts 
b/packages/taler-util/src/talerCrypto.ts
index c20ce72a..d96c2323 100644
--- a/packages/taler-util/src/talerCrypto.ts
+++ b/packages/taler-util/src/talerCrypto.ts
@@ -349,18 +349,25 @@ export function hash(d: Uint8Array): Uint8Array {
   return nacl.hash(d);
 }
 
+/**
+ * Hash a denomination public key according to the
+ * algorithm of exchange protocol v10.
+ */
 export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
-  if (pub.cipher !== DenomKeyType.Rsa) {
-    throw Error("unsupported cipher");
+  if (pub.cipher === DenomKeyType.Rsa) {
+    const pubBuf = decodeCrock(pub.rsa_public_key);
+    const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
+    const uint8ArrayBuf = new Uint8Array(hashInputBuf);
+    const dv = new DataView(hashInputBuf);
+    dv.setUint32(0, pub.age_mask ?? 0);
+    dv.setUint32(4, pub.cipher);
+    uint8ArrayBuf.set(pubBuf, 8);
+    return nacl.hash(uint8ArrayBuf);
+  } else if (pub.cipher === DenomKeyType.LegacyRsa) {
+    return hash(decodeCrock(pub.rsa_public_key));
+  } else {
+    throw Error(`unsupported cipher (${pub.cipher}), unable to hash`);
   }
-  const pubBuf = decodeCrock(pub.rsa_public_key);
-  const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
-  const uint8ArrayBuf = new Uint8Array(hashInputBuf);
-  const dv = new DataView(hashInputBuf);
-  dv.setUint32(0, pub.age_mask ?? 0);
-  dv.setUint32(4, pub.cipher);
-  uint8ArrayBuf.set(pubBuf, 8);
-  return nacl.hash(uint8ArrayBuf);
 }
 
 export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {
diff --git a/packages/taler-util/src/talerTypes.ts 
b/packages/taler-util/src/talerTypes.ts
index bd9c67d7..15dc88ca 100644
--- a/packages/taler-util/src/talerTypes.ts
+++ b/packages/taler-util/src/talerTypes.ts
@@ -38,6 +38,7 @@ import {
   codecForConstNumber,
   buildCodecForUnion,
   codecForConstString,
+  codecForEither,
 } from "./codec.js";
 import {
   Timestamp,
@@ -50,7 +51,7 @@ import { codecForAmountString } from "./amounts.js";
 /**
  * Denomination as found in the /keys response from the exchange.
  */
-export class Denomination {
+export class ExchangeDenomination {
   /**
    * Value of one coin of the denomination.
    */
@@ -58,8 +59,11 @@ export class Denomination {
 
   /**
    * Public signing key of the denomination.
+   *
+   * The "string" alternative is for the old exchange protocol (v9) that
+   * only supports RSA keys.
    */
-  denom_pub: DenominationPubKey;
+  denom_pub: DenominationPubKey | string;
 
   /**
    * Fee for withdrawing.
@@ -128,7 +132,7 @@ export class AuditorDenomSig {
 /**
  * Auditor information as given by the exchange in /keys.
  */
-export class Auditor {
+export class ExchangeAuditor {
   /**
    * Auditor's public key.
    */
@@ -157,8 +161,10 @@ export interface RecoupRequest {
 
   /**
    * Signature over the coin public key by the denomination.
+   * 
+   * The string variant is for the legacy exchange protocol.
    */
-  denom_sig: UnblindedSignature;
+  denom_sig: UnblindedSignature | string;
 
   /**
    * Coin public key of the coin we want to refund.
@@ -198,11 +204,20 @@ export interface RecoupConfirmation {
   old_coin_pub?: string;
 }
 
-export interface UnblindedSignature {
+export type UnblindedSignature =
+  | RsaUnblindedSignature
+  | LegacyRsaUnblindedSignature;
+
+export interface RsaUnblindedSignature {
   cipher: DenomKeyType.Rsa;
   rsa_signature: string;
 }
 
+export interface LegacyRsaUnblindedSignature {
+  cipher: DenomKeyType.LegacyRsa;
+  rsa_signature: string;
+}
+
 /**
  * Deposit permission for a single coin.
  */
@@ -211,18 +226,25 @@ export interface CoinDepositPermission {
    * Signature by the coin.
    */
   coin_sig: string;
+
   /**
    * Public key of the coin being spend.
    */
   coin_pub: string;
+
   /**
    * Signature made by the denomination public key.
+   *
+   * The string variant is for legacy protocol support.
    */
-  ub_sig: UnblindedSignature;
+
+  ub_sig: UnblindedSignature | string;
+
   /**
    * The denomination public key associated with this coin.
    */
   h_denom: string;
+
   /**
    * The amount that is subtracted from this coin with this payment.
    */
@@ -358,6 +380,11 @@ export interface ContractTerms {
    */
   h_wire: string;
 
+  /**
+   * Legacy wire hash, used for deposit operations with an older exchange.
+   */
+  h_wire_legacy?: string;
+
   /**
    * Hash of the merchant's wire details.
    */
@@ -662,7 +689,7 @@ export class ExchangeKeysJson {
   /**
    * List of offered denominations.
    */
-  denoms: Denomination[];
+  denoms: ExchangeDenomination[];
 
   /**
    * The exchange's master public key.
@@ -672,7 +699,7 @@ export class ExchangeKeysJson {
   /**
    * The list of auditors (partially) auditing the exchange.
    */
-  auditors: Auditor[];
+  auditors: ExchangeAuditor[];
 
   /**
    * Timestamp when this response was issued.
@@ -802,6 +829,7 @@ export class TipPickupGetResponse {
 export enum DenomKeyType {
   Rsa = 1,
   ClauseSchnorr = 2,
+  LegacyRsa = 3,
 }
 
 export interface RsaBlindedDenominationSignature {
@@ -809,18 +837,25 @@ export interface RsaBlindedDenominationSignature {
   blinded_rsa_signature: string;
 }
 
+export interface LegacyRsaBlindedDenominationSignature {
+  cipher: DenomKeyType.LegacyRsa;
+  blinded_rsa_signature: string;
+}
+
 export interface CSBlindedDenominationSignature {
   cipher: DenomKeyType.ClauseSchnorr;
 }
 
 export type BlindedDenominationSignature =
   | RsaBlindedDenominationSignature
-  | CSBlindedDenominationSignature;
+  | CSBlindedDenominationSignature
+  | LegacyRsaBlindedDenominationSignature;
 
 export const codecForBlindedDenominationSignature = () =>
   buildCodecForUnion<BlindedDenominationSignature>()
     .discriminateOn("cipher")
     .alternative(1, codecForRsaBlindedDenominationSignature())
+    .alternative(3, codecForLegacyRsaBlindedDenominationSignature())
     .build("BlindedDenominationSignature");
 
 export const codecForRsaBlindedDenominationSignature = () =>
@@ -829,8 +864,17 @@ export const codecForRsaBlindedDenominationSignature = () 
=>
     .property("blinded_rsa_signature", codecForString())
     .build("RsaBlindedDenominationSignature");
 
+export const codecForLegacyRsaBlindedDenominationSignature = () =>
+  buildCodecForObject<LegacyRsaBlindedDenominationSignature>()
+    .property("cipher", codecForConstNumber(1))
+    .property("blinded_rsa_signature", codecForString())
+    .build("LegacyRsaBlindedDenominationSignature");
+
 export class WithdrawResponse {
-  ev_sig: BlindedDenominationSignature;
+  /**
+   * The string variant is for legacy protocol support.
+   */
+  ev_sig: BlindedDenominationSignature | string;
 }
 
 /**
@@ -925,7 +969,10 @@ export interface ExchangeMeltResponse {
 }
 
 export interface ExchangeRevealItem {
-  ev_sig: BlindedDenominationSignature;
+  /**
+   * The string variant is for the legacy v9 protocol.
+   */
+  ev_sig: BlindedDenominationSignature | string;
 }
 
 export interface ExchangeRevealResponse {
@@ -1044,7 +1091,15 @@ export interface BankWithdrawalOperationPostResponse {
   transfer_done: boolean;
 }
 
-export type DenominationPubKey = RsaDenominationPubKey | CsDenominationPubKey;
+export type DenominationPubKey =
+  | RsaDenominationPubKey
+  | CsDenominationPubKey
+  | LegacyRsaDenominationPubKey;
+
+export interface LegacyRsaDenominationPubKey {
+  cipher: DenomKeyType.LegacyRsa;
+  rsa_public_key: string;
+}
 
 export interface RsaDenominationPubKey {
   cipher: DenomKeyType.Rsa;
@@ -1061,6 +1116,7 @@ export const codecForDenominationPubKey = () =>
   buildCodecForUnion<DenominationPubKey>()
     .discriminateOn("cipher")
     .alternative(1, codecForRsaDenominationPubKey())
+    .alternative(3, codecForLegacyRsaDenominationPubKey())
     .build("DenominationPubKey");
 
 export const codecForRsaDenominationPubKey = () =>
@@ -1069,6 +1125,12 @@ export const codecForRsaDenominationPubKey = () =>
     .property("rsa_public_key", codecForString())
     .build("DenominationPubKey");
 
+export const codecForLegacyRsaDenominationPubKey = () =>
+  buildCodecForObject<LegacyRsaDenominationPubKey>()
+    .property("cipher", codecForConstNumber(3))
+    .property("rsa_public_key", codecForString())
+    .build("LegacyRsaDenominationPubKey");
+
 export const codecForBankWithdrawalOperationPostResponse = (): 
Codec<BankWithdrawalOperationPostResponse> =>
   buildCodecForObject<BankWithdrawalOperationPostResponse>()
     .property("transfer_done", codecForBoolean())
@@ -1080,10 +1142,13 @@ export type EddsaSignatureString = string;
 export type EddsaPublicKeyString = string;
 export type CoinPublicKeyString = string;
 
-export const codecForDenomination = (): Codec<Denomination> =>
-  buildCodecForObject<Denomination>()
+export const codecForDenomination = (): Codec<ExchangeDenomination> =>
+  buildCodecForObject<ExchangeDenomination>()
     .property("value", codecForString())
-    .property("denom_pub", codecForDenominationPubKey())
+    .property(
+      "denom_pub",
+      codecForEither(codecForDenominationPubKey(), codecForString()),
+    )
     .property("fee_withdraw", codecForString())
     .property("fee_deposit", codecForString())
     .property("fee_refresh", codecForString())
@@ -1101,8 +1166,8 @@ export const codecForAuditorDenomSig = (): 
Codec<AuditorDenomSig> =>
     .property("auditor_sig", codecForString())
     .build("AuditorDenomSig");
 
-export const codecForAuditor = (): Codec<Auditor> =>
-  buildCodecForObject<Auditor>()
+export const codecForAuditor = (): Codec<ExchangeAuditor> =>
+  buildCodecForObject<ExchangeAuditor>()
     .property("auditor_pub", codecForString())
     .property("auditor_url", codecForString())
     .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
@@ -1261,7 +1326,7 @@ export const codecForExchangeKeysJson = (): 
Codec<ExchangeKeysJson> =>
     .property("signkeys", codecForList(codecForExchangeSigningKey()))
     .property("version", codecForString())
     .property("reserve_closing_delay", codecForDuration)
-    .build("KeysJson");
+    .build("ExchangeKeysJson");
 
 export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
   buildCodecForObject<WireFeesJson>()
@@ -1327,7 +1392,10 @@ export const codecForRecoupConfirmation = (): 
Codec<RecoupConfirmation> =>
 
 export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
   buildCodecForObject<WithdrawResponse>()
-    .property("ev_sig", codecForBlindedDenominationSignature())
+    .property(
+      "ev_sig",
+      codecForEither(codecForBlindedDenominationSignature(), codecForString()),
+    )
     .build("WithdrawResponse");
 
 export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
@@ -1345,7 +1413,10 @@ export const codecForExchangeMeltResponse = (): 
Codec<ExchangeMeltResponse> =>
 
 export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
   buildCodecForObject<ExchangeRevealItem>()
-    .property("ev_sig", codecForBlindedDenominationSignature())
+    .property(
+      "ev_sig",
+      codecForEither(codecForBlindedDenominationSignature(), codecForString()),
+    )
     .build("ExchangeRevealItem");
 
 export const codecForExchangeRevealResponse = (): 
Codec<ExchangeRevealResponse> =>
diff --git a/packages/taler-util/src/walletTypes.ts 
b/packages/taler-util/src/walletTypes.ts
index f00e2907..ced30e4d 100644
--- a/packages/taler-util/src/walletTypes.ts
+++ b/packages/taler-util/src/walletTypes.ts
@@ -49,6 +49,7 @@ import {
   codecForContractTerms,
   ContractTerms,
   DenominationPubKey,
+  DenomKeyType,
   UnblindedSignature,
 } from "./talerTypes.js";
 import { OrderShortInfo, codecForOrderShortInfo } from 
"./transactionsTypes.js";
@@ -515,6 +516,7 @@ export interface DepositInfo {
   merchantPub: string;
   feeDeposit: AmountJson;
   wireInfoHash: string;
+  denomKeyType: DenomKeyType;
   denomPubHash: string;
   denomSig: UnblindedSignature;
 }
diff --git a/packages/taler-wallet-cli/src/harness/harness.ts 
b/packages/taler-wallet-cli/src/harness/harness.ts
index 9a33d572..37a192db 100644
--- a/packages/taler-wallet-cli/src/harness/harness.ts
+++ b/packages/taler-wallet-cli/src/harness/harness.ts
@@ -1173,12 +1173,24 @@ export class ExchangeService implements 
ExchangeServiceInterface {
   }
 
   async runAggregatorOnce() {
-    await runCommand(
-      this.globalState,
-      `exchange-${this.name}-aggregator-once`,
-      "taler-exchange-aggregator",
-      [...this.timetravelArgArr, "-c", this.configFilename, "-t"],
-    );
+    try {
+      await runCommand(
+        this.globalState,
+        `exchange-${this.name}-aggregator-once`,
+        "taler-exchange-aggregator",
+        [...this.timetravelArgArr, "-c", this.configFilename, "-t", "-y"],
+      );
+    } catch (e) {
+      console.log(
+        "running aggregator with KYC off didn't work, might be old version, 
running again",
+      );
+      await runCommand(
+        this.globalState,
+        `exchange-${this.name}-aggregator-once`,
+        "taler-exchange-aggregator",
+        [...this.timetravelArgArr, "-c", this.configFilename, "-t"],
+      );
+    }
   }
 
   async runTransferOnce() {
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index 71431b5e..b57e73a1 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -1018,6 +1018,13 @@ const testCli = walletCli.subcommand("testingArgs", 
"testing", {
   help: "Subcommands for testing.",
 });
 
+testCli.subcommand("logtest", "logtest").action(async (args) => {
+  logger.trace("This is a trace message.");
+  logger.info("This is an info message.");
+  logger.warn("This is an warning message.");
+  logger.error("This is an error message.");
+});
+
 testCli
   .subcommand("listIntegrationtests", "list-integrationtests")
   .action(async (args) => {
diff --git a/packages/taler-wallet-core/src/common.ts 
b/packages/taler-wallet-core/src/common.ts
index 81c43cf1..90c2afdd 100644
--- a/packages/taler-wallet-core/src/common.ts
+++ b/packages/taler-wallet-core/src/common.ts
@@ -51,9 +51,8 @@ export interface TrustInfo {
   isAudited: boolean;
 }
 
-export interface MerchantInfo {
-  supportsMerchantProtocolV1: boolean;
-  supportsMerchantProtocolV2: boolean;
+export interface MerchantInfo { 
+  protocolVersionCurrent: number;
 }
 
 /**
diff --git a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
index e6c0290f..e88b64c3 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoApi.ts
@@ -392,6 +392,7 @@ export class CryptoApi {
   }
 
   isValidWireAccount(
+    versionCurrent: number,
     paytoUri: string,
     sig: string,
     masterPub: string,
@@ -399,6 +400,7 @@ export class CryptoApi {
     return this.doRpc<boolean>(
       "isValidWireAccount",
       4,
+      versionCurrent,
       paytoUri,
       sig,
       masterPub,
diff --git 
a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts 
b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
index 389b98b2..621105b6 100644
--- a/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
+++ b/packages/taler-wallet-core/src/crypto/workers/cryptoImplementation.ts
@@ -154,56 +154,63 @@ export class CryptoImplementation {
    * reserve.
    */
   createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult {
-    if (req.denomPub.cipher !== 1) {
-      throw Error("unsupported cipher");
+    if (
+      req.denomPub.cipher === DenomKeyType.Rsa ||
+      req.denomPub.cipher === DenomKeyType.LegacyRsa
+    ) {
+      const reservePub = decodeCrock(req.reservePub);
+      const reservePriv = decodeCrock(req.reservePriv);
+      const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key);
+      const derivedPlanchet = setupWithdrawPlanchet(
+        decodeCrock(req.secretSeed),
+        req.coinIndex,
+      );
+      const coinPubHash = hash(derivedPlanchet.coinPub);
+      const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa);
+      const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
+      const denomPubHash = hashDenomPub(req.denomPub);
+      const evHash = hash(ev);
+
+      const withdrawRequest = buildSigPS(
+        TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
+      )
+        .put(reservePub)
+        .put(amountToBuffer(amountWithFee))
+        .put(denomPubHash)
+        .put(evHash)
+        .build();
+
+      const sig = eddsaSign(withdrawRequest, reservePriv);
+
+      const planchet: PlanchetCreationResult = {
+        blindingKey: encodeCrock(derivedPlanchet.bks),
+        coinEv: encodeCrock(ev),
+        coinPriv: encodeCrock(derivedPlanchet.coinPriv),
+        coinPub: encodeCrock(derivedPlanchet.coinPub),
+        coinValue: req.value,
+        denomPub: {
+          cipher: req.denomPub.cipher,
+          rsa_public_key: encodeCrock(denomPubRsa),
+        },
+        denomPubHash: encodeCrock(denomPubHash),
+        reservePub: encodeCrock(reservePub),
+        withdrawSig: encodeCrock(sig),
+        coinEvHash: encodeCrock(evHash),
+      };
+      return planchet;
+    } else {
+      throw Error("unsupported cipher, unable to create planchet");
     }
-    const reservePub = decodeCrock(req.reservePub);
-    const reservePriv = decodeCrock(req.reservePriv);
-    const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key);
-    const derivedPlanchet = setupWithdrawPlanchet(
-      decodeCrock(req.secretSeed),
-      req.coinIndex,
-    );
-    const coinPubHash = hash(derivedPlanchet.coinPub);
-    const ev = rsaBlind(coinPubHash, derivedPlanchet.bks, denomPubRsa);
-    const amountWithFee = Amounts.add(req.value, req.feeWithdraw).amount;
-    const denomPubHash = hashDenomPub(req.denomPub);
-    const evHash = hash(ev);
-
-    const withdrawRequest = buildSigPS(
-      TalerSignaturePurpose.WALLET_RESERVE_WITHDRAW,
-    )
-      .put(reservePub)
-      .put(amountToBuffer(amountWithFee))
-      .put(denomPubHash)
-      .put(evHash)
-      .build();
-
-    const sig = eddsaSign(withdrawRequest, reservePriv);
-
-    const planchet: PlanchetCreationResult = {
-      blindingKey: encodeCrock(derivedPlanchet.bks),
-      coinEv: encodeCrock(ev),
-      coinPriv: encodeCrock(derivedPlanchet.coinPriv),
-      coinPub: encodeCrock(derivedPlanchet.coinPub),
-      coinValue: req.value,
-      denomPub: {
-        cipher: 1,
-        rsa_public_key: encodeCrock(denomPubRsa),
-      },
-      denomPubHash: encodeCrock(denomPubHash),
-      reservePub: encodeCrock(reservePub),
-      withdrawSig: encodeCrock(sig),
-      coinEvHash: encodeCrock(evHash),
-    };
-    return planchet;
   }
 
   /**
    * Create a planchet used for tipping, including the private keys.
    */
   createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
-    if (req.denomPub.cipher !== 1) {
+    if (
+      req.denomPub.cipher !== DenomKeyType.Rsa &&
+      req.denomPub.cipher !== DenomKeyType.LegacyRsa
+    ) {
       throw Error("unsupported cipher");
     }
     const fc = setupTipPlanchet(decodeCrock(req.secretSeed), 
req.planchetIndex);
@@ -243,15 +250,29 @@ export class CryptoImplementation {
 
     const coinPriv = decodeCrock(coin.coinPriv);
     const coinSig = eddsaSign(p, coinPriv);
-    const paybackRequest: RecoupRequest = {
-      coin_blind_key_secret: coin.blindingKey,
-      coin_pub: coin.coinPub,
-      coin_sig: encodeCrock(coinSig),
-      denom_pub_hash: coin.denomPubHash,
-      denom_sig: coin.denomSig,
-      refreshed: coin.coinSource.type === CoinSourceType.Refresh,
-    };
-    return paybackRequest;
+    if (coin.denomPub.cipher === DenomKeyType.LegacyRsa) {
+      logger.info("creating legacy recoup request");
+      const paybackRequest: RecoupRequest = {
+        coin_blind_key_secret: coin.blindingKey,
+        coin_pub: coin.coinPub,
+        coin_sig: encodeCrock(coinSig),
+        denom_pub_hash: coin.denomPubHash,
+        denom_sig: coin.denomSig.rsa_signature,
+        refreshed: coin.coinSource.type === CoinSourceType.Refresh,
+      };
+      return paybackRequest;
+    } else {
+      logger.info("creating v10 recoup request");
+      const paybackRequest: RecoupRequest = {
+        coin_blind_key_secret: coin.blindingKey,
+        coin_pub: coin.coinPub,
+        coin_sig: encodeCrock(coinSig),
+        denom_pub_hash: coin.denomPubHash,
+        denom_sig: coin.denomSig,
+        refreshed: coin.coinSource.type === CoinSourceType.Refresh,
+      };
+      return paybackRequest;
+    }
   }
 
   /**
@@ -326,15 +347,31 @@ export class CryptoImplementation {
   }
 
   isValidWireAccount(
+    versionCurrent: number,
     paytoUri: string,
     sig: string,
     masterPub: string,
   ): boolean {
-    const paytoHash = hash(stringToBytes(paytoUri + "\0"));
-    const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
-      .put(paytoHash)
-      .build();
-    return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
+    if (versionCurrent === 10) {
+      const paytoHash = hash(stringToBytes(paytoUri + "\0"));
+      const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
+        .put(paytoHash)
+        .build();
+      return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
+    } else if (versionCurrent === 9) {
+      const h = kdf(
+        64,
+        stringToBytes("exchange-wire-signature"),
+        stringToBytes(paytoUri + "\0"),
+        new Uint8Array(0),
+      );
+      const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
+        .put(h)
+        .build();
+      return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub));
+    } else {
+      throw Error(`unsupported version (${versionCurrent})`);
+    }
   }
 
   isValidContractTermsSignature(
@@ -393,31 +430,64 @@ export class CryptoImplementation {
   signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
     // FIXME: put extensions here if used
     const hExt = new Uint8Array(64);
-    const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
-      .put(decodeCrock(depositInfo.contractTermsHash))
-      .put(hExt)
-      .put(decodeCrock(depositInfo.wireInfoHash))
-      .put(decodeCrock(depositInfo.denomPubHash))
-      .put(timestampRoundedToBuffer(depositInfo.timestamp))
-      .put(timestampRoundedToBuffer(depositInfo.refundDeadline))
-      .put(amountToBuffer(depositInfo.spendAmount))
-      .put(amountToBuffer(depositInfo.feeDeposit))
-      .put(decodeCrock(depositInfo.merchantPub))
-      .build();
+    let d: Uint8Array;
+    if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
+      logger.warn("signing v10 deposit permission");
+      d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
+        .put(decodeCrock(depositInfo.contractTermsHash))
+        .put(hExt)
+        .put(decodeCrock(depositInfo.wireInfoHash))
+        .put(decodeCrock(depositInfo.denomPubHash))
+        .put(timestampRoundedToBuffer(depositInfo.timestamp))
+        .put(timestampRoundedToBuffer(depositInfo.refundDeadline))
+        .put(amountToBuffer(depositInfo.spendAmount))
+        .put(amountToBuffer(depositInfo.feeDeposit))
+        .put(decodeCrock(depositInfo.merchantPub))
+        .build();
+    } else if (depositInfo.denomKeyType === DenomKeyType.LegacyRsa) {
+      logger.warn("signing legacy deposit permission");
+      d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT)
+        .put(decodeCrock(depositInfo.contractTermsHash))
+        .put(decodeCrock(depositInfo.wireInfoHash))
+        .put(decodeCrock(depositInfo.denomPubHash))
+        .put(timestampRoundedToBuffer(depositInfo.timestamp))
+        .put(timestampRoundedToBuffer(depositInfo.refundDeadline))
+        .put(amountToBuffer(depositInfo.spendAmount))
+        .put(amountToBuffer(depositInfo.feeDeposit))
+        .put(decodeCrock(depositInfo.merchantPub))
+        .put(decodeCrock(depositInfo.coinPub))
+        .build();
+    } else {
+      throw Error("unsupported exchange protocol version");
+    }
     const coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
 
-    const s: CoinDepositPermission = {
-      coin_pub: depositInfo.coinPub,
-      coin_sig: encodeCrock(coinSig),
-      contribution: Amounts.stringify(depositInfo.spendAmount),
-      h_denom: depositInfo.denomPubHash,
-      exchange_url: depositInfo.exchangeBaseUrl,
-      ub_sig: {
-        cipher: DenomKeyType.Rsa,
-        rsa_signature: depositInfo.denomSig.rsa_signature,
-      },
-    };
-    return s;
+    if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
+      const s: CoinDepositPermission = {
+        coin_pub: depositInfo.coinPub,
+        coin_sig: encodeCrock(coinSig),
+        contribution: Amounts.stringify(depositInfo.spendAmount),
+        h_denom: depositInfo.denomPubHash,
+        exchange_url: depositInfo.exchangeBaseUrl,
+        ub_sig: {
+          cipher: DenomKeyType.Rsa,
+          rsa_signature: depositInfo.denomSig.rsa_signature,
+        },
+      };
+      return s;
+    } else if (depositInfo.denomKeyType === DenomKeyType.LegacyRsa) {
+      const s: CoinDepositPermission = {
+        coin_pub: depositInfo.coinPub,
+        coin_sig: encodeCrock(coinSig),
+        contribution: Amounts.stringify(depositInfo.spendAmount),
+        h_denom: depositInfo.denomPubHash,
+        exchange_url: depositInfo.exchangeBaseUrl,
+        ub_sig: depositInfo.denomSig.rsa_signature,
+      };
+      return s;
+    } else {
+      throw Error("unsupported merchant protocol version");
+    }
   }
 
   async deriveRefreshSession(
@@ -466,10 +536,12 @@ export class CryptoImplementation {
 
     for (const denomSel of newCoinDenoms) {
       for (let i = 0; i < denomSel.count; i++) {
-        if (denomSel.denomPub.cipher !== 1) {
-          throw Error("unsupported cipher");
+        if (denomSel.denomPub.cipher === DenomKeyType.LegacyRsa) {
+          const r = decodeCrock(denomSel.denomPub.rsa_public_key);
+          sessionHc.update(r);
+        } else {
+          sessionHc.update(hashDenomPub(denomSel.denomPub));
         }
-        sessionHc.update(hashDenomPub(denomSel.denomPub));
       }
     }
 
@@ -508,8 +580,11 @@ export class CryptoImplementation {
             blindingFactor = fresh.bks;
           }
           const pubHash = hash(coinPub);
-          if (denomSel.denomPub.cipher !== 1) {
-            throw Error("unsupported cipher");
+          if (
+            denomSel.denomPub.cipher !== DenomKeyType.Rsa &&
+            denomSel.denomPub.cipher !== DenomKeyType.LegacyRsa
+          ) {
+            throw Error("unsupported cipher, can't create refresh session");
           }
           const denomPub = decodeCrock(denomSel.denomPub.rsa_public_key);
           const ev = rsaBlind(pubHash, blindingFactor, denomPub);
diff --git a/packages/taler-wallet-core/src/db.ts 
b/packages/taler-wallet-core/src/db.ts
index ff47cf30..2d818f1d 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -25,7 +25,7 @@ import {
 import {
   AmountJson,
   AmountString,
-  Auditor,
+  ExchangeAuditor,
   CoinDepositPermission,
   ContractTerms,
   DenominationPubKey,
@@ -427,7 +427,7 @@ export interface ExchangeDetailsRecord {
   /**
    * Auditors (partially) auditing the exchange.
    */
-  auditors: Auditor[];
+  auditors: ExchangeAuditor[];
 
   /**
    * Last observed protocol version.
@@ -1136,6 +1136,7 @@ export interface WalletContractData {
   timestamp: Timestamp;
   wireMethod: string;
   wireInfoHash: string;
+  wireInfoLegacyHash?: string;
   maxDepositFee: AmountJson;
 }
 
diff --git a/packages/taler-wallet-core/src/operations/backup/import.ts 
b/packages/taler-wallet-core/src/operations/backup/import.ts
index 40fa4cde..564d3979 100644
--- a/packages/taler-wallet-core/src/operations/backup/import.ts
+++ b/packages/taler-wallet-core/src/operations/backup/import.ts
@@ -27,6 +27,7 @@ import {
   BackupRefundState,
   RefreshReason,
   BackupRefreshReason,
+  DenomKeyType,
 } from "@gnu-taler/taler-util";
 import {
   WalletContractData,
@@ -331,7 +332,10 @@ export async function importBackup(
         }
 
         for (const backupDenomination of backupExchangeDetails.denominations) {
-          if (backupDenomination.denom_pub.cipher !== 1) {
+          if (
+            backupDenomination.denom_pub.cipher !== DenomKeyType.Rsa &&
+            backupDenomination.denom_pub.cipher !== DenomKeyType.LegacyRsa
+          ) {
             throw Error("unsupported cipher");
           }
           const denomPubHash =
diff --git a/packages/taler-wallet-core/src/operations/backup/index.ts 
b/packages/taler-wallet-core/src/operations/backup/index.ts
index 9027625c..e3950ef9 100644
--- a/packages/taler-wallet-core/src/operations/backup/index.ts
+++ b/packages/taler-wallet-core/src/operations/backup/index.ts
@@ -38,14 +38,15 @@ import {
   codecForString,
   codecOptional,
   ConfirmPayResultType,
+  DenomKeyType,
   durationFromSpec,
   getTimestampNow,
   hashDenomPub,
   HttpStatusCode,
   j2s,
+  LibtoolVersion,
   Logger,
   notEmpty,
-  NotificationType,
   PreparePayResultType,
   RecoveryLoadRequest,
   RecoveryMergeStrategy,
@@ -167,7 +168,10 @@ async function computeBackupCryptoData(
   };
   for (const backupExchangeDetails of backupContent.exchange_details) {
     for (const backupDenom of backupExchangeDetails.denominations) {
-      if (backupDenom.denom_pub.cipher !== 1) {
+      if (
+        backupDenom.denom_pub.cipher !== DenomKeyType.Rsa &&
+        backupDenom.denom_pub.cipher !== DenomKeyType.LegacyRsa
+      ) {
         throw Error("unsupported cipher");
       }
       for (const backupCoin of backupDenom.coins) {
@@ -184,9 +188,25 @@ async function computeBackupCryptoData(
           coinPub,
         };
       }
-      cryptoData.rsaDenomPubToHash[
-        backupDenom.denom_pub.rsa_public_key
-      ] = encodeCrock(hashDenomPub(backupDenom.denom_pub));
+      if (
+        LibtoolVersion.compare(backupExchangeDetails.protocol_version, "9")
+          ?.compatible
+      ) {
+        cryptoData.rsaDenomPubToHash[
+          backupDenom.denom_pub.rsa_public_key
+        ] = encodeCrock(
+          hash(decodeCrock(backupDenom.denom_pub.rsa_public_key)),
+        );
+      } else if (
+        LibtoolVersion.compare(backupExchangeDetails.protocol_version, "10")
+          ?.compatible
+      ) {
+        cryptoData.rsaDenomPubToHash[
+          backupDenom.denom_pub.rsa_public_key
+        ] = encodeCrock(hashDenomPub(backupDenom.denom_pub));
+      } else {
+        throw Error("unsupported exchange protocol version");
+      }
     }
     for (const backupReserve of backupExchangeDetails.reserves) {
       cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index 8fe3702f..f90172a4 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -26,6 +26,7 @@ import {
   CreateDepositGroupRequest,
   CreateDepositGroupResponse,
   decodeCrock,
+  DenomKeyType,
   durationFromSpec,
   getTimestampNow,
   Logger,
@@ -59,6 +60,8 @@ import {
   getCandidatePayCoins,
   getEffectiveDepositAmount,
   getTotalPaymentCost,
+  hashWire,
+  hashWireLegacy,
 } from "./pay.js";
 
 /**
@@ -103,16 +106,6 @@ const codecForDepositSuccess = (): Codec<DepositSuccess> =>
     .property("transaction_base_url", codecOptional(codecForString()))
     .build("DepositSuccess");
 
-function hashWire(paytoUri: string, salt: string): string {
-  const r = kdf(
-    64,
-    stringToBytes(paytoUri + "\0"),
-    decodeCrock(salt),
-    stringToBytes("merchant-wire-signature"),
-  );
-  return encodeCrock(r);
-}
-
 async function resetDepositGroupRetry(
   ws: InternalWalletState,
   depositGroupId: string,
@@ -211,21 +204,50 @@ async function processDepositGroupImpl(
       continue;
     }
     const perm = depositPermissions[i];
+    let requestBody: any;
+    if (
+      typeof perm.ub_sig === "string" ||
+      perm.ub_sig.cipher === DenomKeyType.LegacyRsa
+    ) {
+      // Legacy request
+      logger.info("creating legacy deposit request");
+      const wireHash = hashWireLegacy(
+        depositGroup.wire.payto_uri,
+        depositGroup.wire.salt,
+      );
+      requestBody = {
+        contribution: Amounts.stringify(perm.contribution),
+        wire: depositGroup.wire,
+        h_wire: wireHash,
+        h_contract_terms: depositGroup.contractTermsHash,
+        ub_sig: perm.ub_sig,
+        timestamp: depositGroup.contractTermsRaw.timestamp,
+        wire_transfer_deadline:
+          depositGroup.contractTermsRaw.wire_transfer_deadline,
+        refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
+        coin_sig: perm.coin_sig,
+        denom_pub_hash: perm.h_denom,
+        merchant_pub: depositGroup.merchantPub,
+      };
+    } else {
+      logger.info("creating v10 deposit request");
+      requestBody = {
+        contribution: Amounts.stringify(perm.contribution),
+        merchant_payto_uri: depositGroup.wire.payto_uri,
+        wire_salt: depositGroup.wire.salt,
+        h_contract_terms: depositGroup.contractTermsHash,
+        ub_sig: perm.ub_sig,
+        timestamp: depositGroup.contractTermsRaw.timestamp,
+        wire_transfer_deadline:
+          depositGroup.contractTermsRaw.wire_transfer_deadline,
+        refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
+        coin_sig: perm.coin_sig,
+        denom_pub_hash: perm.h_denom,
+        merchant_pub: depositGroup.merchantPub,
+      };
+    }
     const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
-    const httpResp = await ws.http.postJson(url.href, {
-      contribution: Amounts.stringify(perm.contribution),
-      merchant_payto_uri: depositGroup.wire.payto_uri,
-      wire_salt: depositGroup.wire.salt,
-      h_contract_terms: depositGroup.contractTermsHash,
-      ub_sig: perm.ub_sig,
-      timestamp: depositGroup.contractTermsRaw.timestamp,
-      wire_transfer_deadline:
-        depositGroup.contractTermsRaw.wire_transfer_deadline,
-      refund_deadline: depositGroup.contractTermsRaw.refund_deadline,
-      coin_sig: perm.coin_sig,
-      denom_pub_hash: perm.h_denom,
-      merchant_pub: depositGroup.merchantPub,
-    });
+    const httpResp = await ws.http.postJson(url.href, requestBody);
     await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
     await ws.db
       .mktx((x) => ({ depositGroups: x.depositGroups }))
@@ -358,6 +380,7 @@ export async function createDepositGroup(
   const merchantPair = await ws.cryptoApi.createEddsaKeypair();
   const wireSalt = encodeCrock(getRandomBytes(16));
   const wireHash = hashWire(req.depositPaytoUri, wireSalt);
+  const wireHashLegacy = hashWireLegacy(req.depositPaytoUri, wireSalt);
   const contractTerms: ContractTerms = {
     auditors: [],
     exchanges: exchangeInfos,
@@ -371,7 +394,10 @@ export async function createDepositGroup(
     nonce: noncePair.pub,
     wire_transfer_deadline: timestampRound,
     order_id: "",
+    // This is always the v2 wire hash, as we're the "merchant" and support v2.
     h_wire: wireHash,
+    // Required for older exchanges.
+    h_wire_legacy: wireHashLegacy,
     pay_deadline: timestampAddDuration(
       timestampRound,
       durationFromSpec({ hours: 1 }),
diff --git a/packages/taler-wallet-core/src/operations/exchanges.ts 
b/packages/taler-wallet-core/src/operations/exchanges.ts
index a10378a8..16e37fd3 100644
--- a/packages/taler-wallet-core/src/operations/exchanges.ts
+++ b/packages/taler-wallet-core/src/operations/exchanges.ts
@@ -19,11 +19,11 @@
  */
 import {
   Amounts,
-  Auditor,
+  ExchangeAuditor,
   canonicalizeBaseUrl,
   codecForExchangeKeysJson,
   codecForExchangeWireJson,
-  Denomination,
+  ExchangeDenomination,
   Duration,
   durationFromSpec,
   ExchangeSignKeyJson,
@@ -40,6 +40,9 @@ import {
   Timestamp,
   hashDenomPub,
   LibtoolVersion,
+  codecForAny,
+  DenominationPubKey,
+  DenomKeyType,
 } from "@gnu-taler/taler-util";
 import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
 import { CryptoApi } from "../crypto/workers/cryptoApi.js";
@@ -77,11 +80,21 @@ function denominationRecordFromKeys(
   exchangeBaseUrl: string,
   exchangeMasterPub: string,
   listIssueDate: Timestamp,
-  denomIn: Denomination,
+  denomIn: ExchangeDenomination,
 ): DenominationRecord {
-  const denomPubHash = encodeCrock(hashDenomPub(denomIn.denom_pub));
+  let denomPub: DenominationPubKey;
+  // We support exchange protocol v9 and v10.
+  if (typeof denomIn.denom_pub === "string") {
+    denomPub = {
+      cipher: DenomKeyType.LegacyRsa,
+      rsa_public_key: denomIn.denom_pub,
+    };
+  } else {
+    denomPub = denomIn.denom_pub;
+  }
+  const denomPubHash = encodeCrock(hashDenomPub(denomPub));
   const d: DenominationRecord = {
-    denomPub: denomIn.denom_pub,
+    denomPub,
     denomPubHash,
     exchangeBaseUrl,
     exchangeMasterPub,
@@ -205,6 +218,7 @@ export async function acceptExchangeTermsOfService(
 }
 
 async function validateWireInfo(
+  versionCurrent: number,
   wireInfo: ExchangeWireJson,
   masterPublicKey: string,
   cryptoApi: CryptoApi,
@@ -212,6 +226,7 @@ async function validateWireInfo(
   for (const a of wireInfo.accounts) {
     logger.trace("validating exchange acct");
     const isValid = await cryptoApi.isValidWireAccount(
+      versionCurrent,
       a.payto_uri,
       a.master_sig,
       masterPublicKey,
@@ -321,7 +336,7 @@ async function provideExchangeRecord(
 interface ExchangeKeysDownloadResult {
   masterPublicKey: string;
   currency: string;
-  auditors: Auditor[];
+  auditors: ExchangeAuditor[];
   currentDenominations: DenominationRecord[];
   protocolVersion: string;
   signingKeys: ExchangeSignKeyJson[];
@@ -345,14 +360,14 @@ async function downloadKeysInfo(
   const resp = await http.get(keysUrl.href, {
     timeout,
   });
-  const exchangeKeysJson = await readSuccessResponseJsonOrThrow(
+  const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
     resp,
     codecForExchangeKeysJson(),
   );
 
   logger.info("received /keys response");
 
-  if (exchangeKeysJson.denoms.length === 0) {
+  if (exchangeKeysJsonUnchecked.denoms.length === 0) {
     const opErr = makeErrorDetails(
       TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
       "exchange doesn't offer any denominations",
@@ -363,7 +378,7 @@ async function downloadKeysInfo(
     throw new OperationFailedError(opErr);
   }
 
-  const protocolVersion = exchangeKeysJson.version;
+  const protocolVersion = exchangeKeysJsonUnchecked.version;
 
   const versionRes = LibtoolVersion.compare(
     WALLET_EXCHANGE_PROTOCOL_VERSION,
@@ -382,29 +397,29 @@ async function downloadKeysInfo(
   }
 
   const currency = Amounts.parseOrThrow(
-    exchangeKeysJson.denoms[0].value,
+    exchangeKeysJsonUnchecked.denoms[0].value,
   ).currency.toUpperCase();
 
   return {
-    masterPublicKey: exchangeKeysJson.master_public_key,
+    masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
     currency,
-    auditors: exchangeKeysJson.auditors,
-    currentDenominations: exchangeKeysJson.denoms.map((d) =>
+    auditors: exchangeKeysJsonUnchecked.auditors,
+    currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
       denominationRecordFromKeys(
         baseUrl,
-        exchangeKeysJson.master_public_key,
-        exchangeKeysJson.list_issue_date,
+        exchangeKeysJsonUnchecked.master_public_key,
+        exchangeKeysJsonUnchecked.list_issue_date,
         d,
       ),
     ),
-    protocolVersion: exchangeKeysJson.version,
-    signingKeys: exchangeKeysJson.signkeys,
-    reserveClosingDelay: exchangeKeysJson.reserve_closing_delay,
+    protocolVersion: exchangeKeysJsonUnchecked.version,
+    signingKeys: exchangeKeysJsonUnchecked.signkeys,
+    reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
     expiry: getExpiryTimestamp(resp, {
       minDuration: durationFromSpec({ hours: 1 }),
     }),
-    recoup: exchangeKeysJson.recoup ?? [],
-    listIssueDate: exchangeKeysJson.list_issue_date,
+    recoup: exchangeKeysJsonUnchecked.recoup ?? [],
+    listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
   };
 }
 
@@ -466,7 +481,14 @@ async function updateExchangeFromUrlImpl(
 
   logger.info("validating exchange /wire info");
 
+  const version = LibtoolVersion.parseVersion(keysInfo.protocolVersion);
+  if (!version) {
+    // Should have been validated earlier.
+    throw Error("unexpected invalid version");
+  }
+
   const wireInfo = await validateWireInfo(
+    version.current,
     wireInfoDownload,
     keysInfo.masterPublicKey,
     ws.cryptoApi,
diff --git a/packages/taler-wallet-core/src/operations/merchants.ts 
b/packages/taler-wallet-core/src/operations/merchants.ts
index d12417c7..fd628fa9 100644
--- a/packages/taler-wallet-core/src/operations/merchants.ts
+++ b/packages/taler-wallet-core/src/operations/merchants.ts
@@ -52,15 +52,13 @@ export async function getMerchantInfo(
     `merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`,
   );
 
+  const parsedVersion = LibtoolVersion.parseVersion(configResp.version);
+  if (!parsedVersion) {
+    throw Error("invalid merchant version");
+  }
+
   const merchantInfo: MerchantInfo = {
-    supportsMerchantProtocolV1: !!LibtoolVersion.compare(
-      "1:0:0",
-      configResp.version,
-    )?.compatible,
-    supportsMerchantProtocolV2: !!LibtoolVersion.compare(
-      "2:0:0",
-      configResp.version,
-    )?.compatible,
+    protocolVersionCurrent: parsedVersion.current,
   };
 
   ws.merchantInfoCache[canonBaseUrl] = merchantInfo;
diff --git a/packages/taler-wallet-core/src/operations/pay.ts 
b/packages/taler-wallet-core/src/operations/pay.ts
index acc592a7..73fc6537 100644
--- a/packages/taler-wallet-core/src/operations/pay.ts
+++ b/packages/taler-wallet-core/src/operations/pay.ts
@@ -54,6 +54,10 @@ import {
   URL,
   getDurationRemaining,
   HttpStatusCode,
+  DenomKeyType,
+  kdf,
+  stringToBytes,
+  decodeCrock,
 } from "@gnu-taler/taler-util";
 import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
 import {
@@ -108,6 +112,26 @@ import {
  */
 const logger = new Logger("pay.ts");
 
+export function hashWire(paytoUri: string, salt: string): string {
+  const r = kdf(
+    64,
+    stringToBytes(paytoUri + "\0"),
+    decodeCrock(salt),
+    stringToBytes("merchant-wire-signature"),
+  );
+  return encodeCrock(r);
+}
+
+export function hashWireLegacy(paytoUri: string, salt: string): string {
+  const r = kdf(
+    64,
+    stringToBytes(paytoUri + "\0"),
+    stringToBytes(salt + "\0"),
+    stringToBytes("merchant-wire-signature"),
+  );
+  return encodeCrock(r);
+}
+
 /**
  * Compute the total cost of a payment to the customer.
  *
@@ -193,9 +217,9 @@ export async function getEffectiveDepositAmount(
         if (!exchangeDetails) {
           continue;
         }
-       // FIXME/NOTE: the line below _likely_ throws exception
-       // about "find method not found on undefined" when the wireType
-       // is not supported by the Exchange.
+        // FIXME/NOTE: the line below _likely_ throws exception
+        // about "find method not found on undefined" when the wireType
+        // is not supported by the Exchange.
         const fee = exchangeDetails.wireInfo.feesForType[wireType].find((x) => 
{
           return timestampIsBetween(
             getTimestampNow(),
@@ -669,6 +693,7 @@ export function extractContractData(
     timestamp: parsedContractTerms.timestamp,
     wireMethod: parsedContractTerms.wire_method,
     wireInfoHash: parsedContractTerms.h_wire,
+    wireInfoLegacyHash: parsedContractTerms.h_wire_legacy,
     maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
     merchant: parsedContractTerms.merchant,
     products: parsedContractTerms.products,
@@ -882,7 +907,6 @@ async function startDownloadProposal(
   claimToken: string | undefined,
   noncePriv: string | undefined,
 ): Promise<string> {
-
   const oldProposal = await ws.db
     .mktx((x) => ({ proposals: x.proposals }))
     .runReadOnly(async (tx) => {
@@ -891,20 +915,24 @@ async function startDownloadProposal(
         orderId,
       ]);
     });
-  
+
   /**
    * If we have already claimed this proposal with the same sessionId
    * nonce and claim token, reuse it.
    */
-  if (oldProposal && 
-      oldProposal.downloadSessionId === sessionId &&
-      (!noncePriv || oldProposal.noncePriv === noncePriv) &&
-      oldProposal.claimToken === claimToken) {
+  if (
+    oldProposal &&
+    oldProposal.downloadSessionId === sessionId &&
+    (!noncePriv || oldProposal.noncePriv === noncePriv) &&
+    oldProposal.claimToken === claimToken
+  ) {
     await processDownloadProposal(ws, oldProposal.proposalId);
     return oldProposal.proposalId;
   }
 
-  const { priv, pub } = await (noncePriv ? 
ws.cryptoApi.eddsaGetPublic(noncePriv) : ws.cryptoApi.createEddsaKeypair());
+  const { priv, pub } = await (noncePriv
+    ? ws.cryptoApi.eddsaGetPublic(noncePriv)
+    : ws.cryptoApi.createEddsaKeypair());
   const proposalId = encodeCrock(getRandomBytes(32));
 
   const proposalRecord: ProposalRecord = {
@@ -1169,6 +1197,11 @@ async function submitPay(
 
   logger.trace("paying with session ID", sessionId);
 
+  const merchantInfo = await ws.merchantOps.getMerchantInfo(
+    ws,
+    purchase.download.contractData.merchantBaseUrl,
+  );
+
   if (!purchase.merchantPaySig) {
     const payUrl = new URL(
       `orders/${purchase.download.contractData.orderId}/pay`,
@@ -1568,11 +1601,21 @@ export async function generateDepositPermissions(
 
   for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
     const { coin, denom } = coinWithDenom[i];
+    let wireInfoHash: string;
+    if (
+      coin.denomPub.cipher === DenomKeyType.LegacyRsa &&
+      contractData.wireInfoLegacyHash
+    ) {
+      wireInfoHash = contractData.wireInfoLegacyHash;
+    } else {
+      wireInfoHash = contractData.wireInfoHash;
+    }
     const dp = await ws.cryptoApi.signDepositPermission({
       coinPriv: coin.coinPriv,
       coinPub: coin.coinPub,
       contractTermsHash: contractData.contractTermsHash,
       denomPubHash: coin.denomPubHash,
+      denomKeyType: coin.denomPub.cipher,
       denomSig: coin.denomSig,
       exchangeBaseUrl: coin.exchangeBaseUrl,
       feeDeposit: denom.feeDeposit,
@@ -1580,7 +1623,7 @@ export async function generateDepositPermissions(
       refundDeadline: contractData.refundDeadline,
       spendAmount: payCoinSel.coinContributions[i],
       timestamp: contractData.timestamp,
-      wireInfoHash: contractData.wireInfoHash,
+      wireInfoHash,
     });
     depositPermissions.push(dp);
   }
@@ -1613,6 +1656,11 @@ export async function confirmPay(
     throw Error("proposal is in invalid state");
   }
 
+  const merchantInfo = await ws.merchantOps.getMerchantInfo(
+    ws,
+    d.contractData.merchantBaseUrl,
+  );
+
   const existingPurchase = await ws.db
     .mktx((x) => ({ purchases: x.purchases }))
     .runReadWrite(async (tx) => {
diff --git a/packages/taler-wallet-core/src/operations/refresh.ts 
b/packages/taler-wallet-core/src/operations/refresh.ts
index c1e672d6..51eac4a6 100644
--- a/packages/taler-wallet-core/src/operations/refresh.ts
+++ b/packages/taler-wallet-core/src/operations/refresh.ts
@@ -365,18 +365,29 @@ async function refreshMelt(
     `coins/${oldCoin.coinPub}/melt`,
     oldCoin.exchangeBaseUrl,
   );
-  const meltReq = {
-    coin_pub: oldCoin.coinPub,
-    confirm_sig: derived.confirmSig,
-    denom_pub_hash: oldCoin.denomPubHash,
-    denom_sig: oldCoin.denomSig,
-    rc: derived.hash,
-    value_with_fee: Amounts.stringify(derived.meltValueWithFee),
-  };
-  logger.trace(`melt request for coin:`, meltReq);
+  let meltReqBody: any;
+  if (oldCoin.denomPub.cipher === DenomKeyType.LegacyRsa) {
+    meltReqBody = {
+      coin_pub: oldCoin.coinPub,
+      confirm_sig: derived.confirmSig,
+      denom_pub_hash: oldCoin.denomPubHash,
+      denom_sig: oldCoin.denomSig.rsa_signature,
+      rc: derived.hash,
+      value_with_fee: Amounts.stringify(derived.meltValueWithFee),
+    };
+  } else {
+    meltReqBody = {
+      coin_pub: oldCoin.coinPub,
+      confirm_sig: derived.confirmSig,
+      denom_pub_hash: oldCoin.denomPubHash,
+      denom_sig: oldCoin.denomSig,
+      rc: derived.hash,
+      value_with_fee: Amounts.stringify(derived.meltValueWithFee),
+    };
+  }
 
   const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => {
-    return await ws.http.postJson(reqUrl.href, meltReq, {
+    return await ws.http.postJson(reqUrl.href, meltReqBody, {
       timeout: getRefreshRequestTimeout(refreshGroup),
     });
   });
@@ -604,15 +615,26 @@ async function refreshReveal(
         continue;
       }
       const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex];
-      if (denom.denomPub.cipher !== 1) {
+      if (
+        denom.denomPub.cipher !== DenomKeyType.Rsa &&
+        denom.denomPub.cipher !== DenomKeyType.LegacyRsa
+      ) {
         throw Error("cipher unsupported");
       }
       const evSig = reveal.ev_sigs[newCoinIndex].ev_sig;
-      if (evSig.cipher !== DenomKeyType.Rsa) {
+      let rsaSig: string;
+      if (typeof evSig === "string") {
+        rsaSig = evSig;
+      } else if (
+        evSig.cipher === DenomKeyType.Rsa ||
+        evSig.cipher === DenomKeyType.LegacyRsa
+      ) {
+        rsaSig = evSig.blinded_rsa_signature;
+      } else {
         throw Error("unsupported cipher");
       }
       const denomSigRsa = await ws.cryptoApi.rsaUnblind(
-        evSig.blinded_rsa_signature,
+        rsaSig,
         pc.blindingKey,
         denom.denomPub.rsa_public_key,
       );
diff --git a/packages/taler-wallet-core/src/operations/tip.ts 
b/packages/taler-wallet-core/src/operations/tip.ts
index 0253930e..cf3502ec 100644
--- a/packages/taler-wallet-core/src/operations/tip.ts
+++ b/packages/taler-wallet-core/src/operations/tip.ts
@@ -314,13 +314,13 @@ async function processTipImpl(
 
   let blindedSigs: BlindedDenominationSignature[] = [];
 
-  if (merchantInfo.supportsMerchantProtocolV2) {
+  if (merchantInfo.protocolVersionCurrent === 2) {
     const response = await readSuccessResponseJsonOrThrow(
       merchantResp,
       codecForMerchantTipResponseV2(),
     );
     blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
-  } else if (merchantInfo.supportsMerchantProtocolV1) {
+  } else if (merchantInfo.protocolVersionCurrent === 1) {
     const response = await readSuccessResponseJsonOrThrow(
       merchantResp,
       codecForMerchantTipResponseV1(),
@@ -347,11 +347,17 @@ async function processTipImpl(
     const planchet = planchets[i];
     checkLogicInvariant(!!planchet);
 
-    if (denom.denomPub.cipher !== DenomKeyType.Rsa) {
+    if (
+      denom.denomPub.cipher !== DenomKeyType.Rsa &&
+      denom.denomPub.cipher !== DenomKeyType.LegacyRsa
+    ) {
       throw Error("unsupported cipher");
     }
 
-    if (blindedSig.cipher !== DenomKeyType.Rsa) {
+    if (
+      blindedSig.cipher !== DenomKeyType.Rsa &&
+      blindedSig.cipher !== DenomKeyType.LegacyRsa
+    ) {
       throw Error("unsupported cipher");
     }
 
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 979bd0e5..8c9178f5 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -42,6 +42,7 @@ import {
   VersionMatchResult,
   DenomKeyType,
   LibtoolVersion,
+  UnblindedSignature,
 } from "@gnu-taler/taler-util";
 import {
   CoinRecord,
@@ -591,12 +592,28 @@ async function processPlanchetVerifyAndStoreCoin(
   const { planchet, exchangeBaseUrl } = d;
 
   const planchetDenomPub = planchet.denomPub;
-  if (planchetDenomPub.cipher !== DenomKeyType.Rsa) {
-    throw Error("cipher not supported");
+  if (
+    planchetDenomPub.cipher !== DenomKeyType.Rsa &&
+    planchetDenomPub.cipher !== DenomKeyType.LegacyRsa
+  ) {
+    throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
   }
 
-  const evSig = resp.ev_sig;
-  if (evSig.cipher !== DenomKeyType.Rsa) {
+  let evSig = resp.ev_sig;
+  if (typeof resp.ev_sig === "string") {
+    evSig = {
+      cipher: DenomKeyType.LegacyRsa,
+      blinded_rsa_signature: resp.ev_sig,
+    };
+  } else {
+    evSig = resp.ev_sig;
+  }
+  if (
+    !(
+      evSig.cipher === DenomKeyType.Rsa ||
+      evSig.cipher === DenomKeyType.LegacyRsa
+    )
+  ) {
     throw Error("unsupported cipher");
   }
 
@@ -633,6 +650,19 @@ async function processPlanchetVerifyAndStoreCoin(
     return;
   }
 
+  let denomSig: UnblindedSignature;
+  if (
+    planchet.denomPub.cipher === DenomKeyType.LegacyRsa ||
+    planchet.denomPub.cipher === DenomKeyType.Rsa
+  ) {
+    denomSig = {
+      cipher: planchet.denomPub.cipher,
+      rsa_signature: denomSigRsa,
+    };
+  } else {
+    throw Error("unsupported cipher");
+  }
+
   const coin: CoinRecord = {
     blindingKey: planchet.blindingKey,
     coinPriv: planchet.coinPriv,
@@ -640,10 +670,7 @@ async function processPlanchetVerifyAndStoreCoin(
     currentAmount: planchet.coinValue,
     denomPub: planchet.denomPub,
     denomPubHash: planchet.denomPubHash,
-    denomSig: {
-      cipher: DenomKeyType.Rsa,
-      rsa_signature: denomSigRsa,
-    },
+    denomSig,
     coinEvHash: planchet.coinEvHash,
     exchangeBaseUrl: exchangeBaseUrl,
     status: CoinStatus.Fresh,
diff --git a/packages/taler-wallet-core/src/util/coinSelection.ts 
b/packages/taler-wallet-core/src/util/coinSelection.ts
index ba26c98f..bfc481ea 100644
--- a/packages/taler-wallet-core/src/util/coinSelection.ts
+++ b/packages/taler-wallet-core/src/util/coinSelection.ts
@@ -23,7 +23,12 @@
 /**
  * Imports.
  */
-import { AmountJson, Amounts, DenominationPubKey } from 
"@gnu-taler/taler-util";
+import {
+  AmountJson,
+  Amounts,
+  DenominationPubKey,
+  DenomKeyType,
+} from "@gnu-taler/taler-util";
 import { strcmp, Logger } from "@gnu-taler/taler-util";
 
 const logger = new Logger("coinSelection.ts");
@@ -215,10 +220,21 @@ function denomPubCmp(
   } else if (p1.cipher > p2.cipher) {
     return +1;
   }
-  if (p1.cipher !== 1 || p2.cipher !== 1) {
+  if (
+    p1.cipher === DenomKeyType.LegacyRsa &&
+    p2.cipher === DenomKeyType.LegacyRsa
+  ) {
+    return strcmp(p1.rsa_public_key, p2.rsa_public_key);
+  } else if (p1.cipher === DenomKeyType.Rsa && p2.cipher === DenomKeyType.Rsa) 
{
+    if ((p1.age_mask ?? 0) < (p2.age_mask ?? 0)) {
+      return -1;
+    } else if ((p1.age_mask ?? 0) > (p2.age_mask ?? 0)) {
+      return 1;
+    }
+    return strcmp(p1.rsa_public_key, p2.rsa_public_key);
+  } else {
     throw Error("unsupported cipher");
   }
-  return strcmp(p1.rsa_public_key, p2.rsa_public_key);
 }
 
 /**
diff --git a/packages/taler-wallet-core/src/versions.ts 
b/packages/taler-wallet-core/src/versions.ts
index 7383355b..9ef298d6 100644
--- a/packages/taler-wallet-core/src/versions.ts
+++ b/packages/taler-wallet-core/src/versions.ts
@@ -19,14 +19,14 @@
  *
  * Uses libtool's current:revision:age versioning.
  */
-export const WALLET_EXCHANGE_PROTOCOL_VERSION = "10:0:0";
+export const WALLET_EXCHANGE_PROTOCOL_VERSION = "10:0:1";
 
 /**
  * Protocol version spoken with the merchant.
  *
  * Uses libtool's current:revision:age versioning.
  */
-export const WALLET_MERCHANT_PROTOCOL_VERSION = "1:0:0";
+export const WALLET_MERCHANT_PROTOCOL_VERSION = "2:0:1";
 
 /**
  * Protocol version spoken with the merchant.
@@ -42,4 +42,4 @@ export const WALLET_BANK_INTEGRATION_PROTOCOL_VERSION = 
"0:0:0";
  *
  * This is only a temporary measure.
  */
-export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "3";
+export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "4";
diff --git a/packages/taler-wallet-core/src/wallet.ts 
b/packages/taler-wallet-core/src/wallet.ts
index 576a4459..7233af3a 100644
--- a/packages/taler-wallet-core/src/wallet.ts
+++ b/packages/taler-wallet-core/src/wallet.ts
@@ -390,7 +390,7 @@ async function runTaskLoop(
         } catch (e) {
           if (e instanceof OperationFailedAndReportedError) {
             logger.warn("operation processed resulted in reported error");
-            logger.warn(`reporred error was: ${j2s(e.operationError)}`);
+            logger.warn(`reported error was: ${j2s(e.operationError)}`);
           } else {
             logger.error("Uncaught exception", e);
             ws.notify({
@@ -985,6 +985,8 @@ export async function handleCoreApiRequest(
       e instanceof OperationFailedError ||
       e instanceof OperationFailedAndReportedError
     ) {
+      logger.error("Caught operation failed error");
+      logger.trace((e as any).stack);
       return {
         type: "error",
         operation,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]