wallet: support both protocol versions

This commit is contained in:
Florian Dold 2021-11-27 20:56:58 +01:00
parent 403de8170e
commit 5c4c25516d
No known key found for this signature in database
GPG Key ID: D2E4F00F29D02A4B
24 changed files with 623 additions and 231 deletions

View File

@ -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());

View File

@ -27,14 +27,15 @@ export interface VersionMatchResult {
* Is the first version compatible with the second? * Is the first version compatible with the second?
*/ */
compatible: boolean; compatible: boolean;
/** /**
* Is the first version older (-1), newser (+1) or * Is the first version older (-1), newer (+1) or
* identical (0)? * identical (0)?
*/ */
currentCmp: number; currentCmp: number;
} }
interface Version { export interface Version {
current: number; current: number;
revision: number; revision: number;
age: number; age: number;
@ -64,7 +65,7 @@ export namespace LibtoolVersion {
return { compatible, currentCmp }; return { compatible, currentCmp };
} }
function parseVersion(v: string): Version | undefined { export function parseVersion(v: string): Version | undefined {
const [currentStr, revisionStr, ageStr, ...rest] = v.split(":"); const [currentStr, revisionStr, ageStr, ...rest] = v.split(":");
if (rest.length !== 0) { if (rest.length !== 0) {
return undefined; return undefined;

View File

@ -55,7 +55,7 @@ export function setGlobalLogLevelFromString(logLevelStr: string) {
break; break;
default: default:
if (isNode) { if (isNode) {
process.stderr.write(`Invalid log level, defaulting to WARNING`); process.stderr.write(`Invalid log level, defaulting to WARNING\n`);
} else { } else {
console.warn(`Invalid log level, defaulting to WARNING`); console.warn(`Invalid log level, defaulting to WARNING`);
} }
@ -143,6 +143,7 @@ export class Logger {
case LogLevel.Info: case LogLevel.Info:
case LogLevel.Warn: case LogLevel.Warn:
case LogLevel.Error: case LogLevel.Error:
return true;
case LogLevel.None: case LogLevel.None:
return false; return false;
} }

View File

@ -349,10 +349,12 @@ export function hash(d: Uint8Array): Uint8Array {
return nacl.hash(d); return nacl.hash(d);
} }
/**
* Hash a denomination public key according to the
* algorithm of exchange protocol v10.
*/
export function hashDenomPub(pub: DenominationPubKey): Uint8Array { export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
if (pub.cipher !== DenomKeyType.Rsa) { if (pub.cipher === DenomKeyType.Rsa) {
throw Error("unsupported cipher");
}
const pubBuf = decodeCrock(pub.rsa_public_key); const pubBuf = decodeCrock(pub.rsa_public_key);
const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4); const hashInputBuf = new ArrayBuffer(pubBuf.length + 4 + 4);
const uint8ArrayBuf = new Uint8Array(hashInputBuf); const uint8ArrayBuf = new Uint8Array(hashInputBuf);
@ -361,6 +363,11 @@ export function hashDenomPub(pub: DenominationPubKey): Uint8Array {
dv.setUint32(4, pub.cipher); dv.setUint32(4, pub.cipher);
uint8ArrayBuf.set(pubBuf, 8); uint8ArrayBuf.set(pubBuf, 8);
return nacl.hash(uint8ArrayBuf); 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`);
}
} }
export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array { export function eddsaSign(msg: Uint8Array, eddsaPriv: Uint8Array): Uint8Array {

View File

@ -38,6 +38,7 @@ import {
codecForConstNumber, codecForConstNumber,
buildCodecForUnion, buildCodecForUnion,
codecForConstString, codecForConstString,
codecForEither,
} from "./codec.js"; } from "./codec.js";
import { import {
Timestamp, Timestamp,
@ -50,7 +51,7 @@ import { codecForAmountString } from "./amounts.js";
/** /**
* Denomination as found in the /keys response from the exchange. * Denomination as found in the /keys response from the exchange.
*/ */
export class Denomination { export class ExchangeDenomination {
/** /**
* Value of one coin of the denomination. * Value of one coin of the denomination.
*/ */
@ -58,8 +59,11 @@ export class Denomination {
/** /**
* Public signing key of the 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. * Fee for withdrawing.
@ -128,7 +132,7 @@ export class AuditorDenomSig {
/** /**
* Auditor information as given by the exchange in /keys. * Auditor information as given by the exchange in /keys.
*/ */
export class Auditor { export class ExchangeAuditor {
/** /**
* Auditor's public key. * Auditor's public key.
*/ */
@ -157,8 +161,10 @@ export interface RecoupRequest {
/** /**
* Signature over the coin public key by the denomination. * 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. * Coin public key of the coin we want to refund.
@ -198,11 +204,20 @@ export interface RecoupConfirmation {
old_coin_pub?: string; old_coin_pub?: string;
} }
export interface UnblindedSignature { export type UnblindedSignature =
| RsaUnblindedSignature
| LegacyRsaUnblindedSignature;
export interface RsaUnblindedSignature {
cipher: DenomKeyType.Rsa; cipher: DenomKeyType.Rsa;
rsa_signature: string; rsa_signature: string;
} }
export interface LegacyRsaUnblindedSignature {
cipher: DenomKeyType.LegacyRsa;
rsa_signature: string;
}
/** /**
* Deposit permission for a single coin. * Deposit permission for a single coin.
*/ */
@ -211,18 +226,25 @@ export interface CoinDepositPermission {
* Signature by the coin. * Signature by the coin.
*/ */
coin_sig: string; coin_sig: string;
/** /**
* Public key of the coin being spend. * Public key of the coin being spend.
*/ */
coin_pub: string; coin_pub: string;
/** /**
* Signature made by the denomination public key. * 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. * The denomination public key associated with this coin.
*/ */
h_denom: string; h_denom: string;
/** /**
* The amount that is subtracted from this coin with this payment. * The amount that is subtracted from this coin with this payment.
*/ */
@ -358,6 +380,11 @@ export interface ContractTerms {
*/ */
h_wire: string; 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. * Hash of the merchant's wire details.
*/ */
@ -662,7 +689,7 @@ export class ExchangeKeysJson {
/** /**
* List of offered denominations. * List of offered denominations.
*/ */
denoms: Denomination[]; denoms: ExchangeDenomination[];
/** /**
* The exchange's master public key. * The exchange's master public key.
@ -672,7 +699,7 @@ export class ExchangeKeysJson {
/** /**
* The list of auditors (partially) auditing the exchange. * The list of auditors (partially) auditing the exchange.
*/ */
auditors: Auditor[]; auditors: ExchangeAuditor[];
/** /**
* Timestamp when this response was issued. * Timestamp when this response was issued.
@ -802,6 +829,7 @@ export class TipPickupGetResponse {
export enum DenomKeyType { export enum DenomKeyType {
Rsa = 1, Rsa = 1,
ClauseSchnorr = 2, ClauseSchnorr = 2,
LegacyRsa = 3,
} }
export interface RsaBlindedDenominationSignature { export interface RsaBlindedDenominationSignature {
@ -809,18 +837,25 @@ export interface RsaBlindedDenominationSignature {
blinded_rsa_signature: string; blinded_rsa_signature: string;
} }
export interface LegacyRsaBlindedDenominationSignature {
cipher: DenomKeyType.LegacyRsa;
blinded_rsa_signature: string;
}
export interface CSBlindedDenominationSignature { export interface CSBlindedDenominationSignature {
cipher: DenomKeyType.ClauseSchnorr; cipher: DenomKeyType.ClauseSchnorr;
} }
export type BlindedDenominationSignature = export type BlindedDenominationSignature =
| RsaBlindedDenominationSignature | RsaBlindedDenominationSignature
| CSBlindedDenominationSignature; | CSBlindedDenominationSignature
| LegacyRsaBlindedDenominationSignature;
export const codecForBlindedDenominationSignature = () => export const codecForBlindedDenominationSignature = () =>
buildCodecForUnion<BlindedDenominationSignature>() buildCodecForUnion<BlindedDenominationSignature>()
.discriminateOn("cipher") .discriminateOn("cipher")
.alternative(1, codecForRsaBlindedDenominationSignature()) .alternative(1, codecForRsaBlindedDenominationSignature())
.alternative(3, codecForLegacyRsaBlindedDenominationSignature())
.build("BlindedDenominationSignature"); .build("BlindedDenominationSignature");
export const codecForRsaBlindedDenominationSignature = () => export const codecForRsaBlindedDenominationSignature = () =>
@ -829,8 +864,17 @@ export const codecForRsaBlindedDenominationSignature = () =>
.property("blinded_rsa_signature", codecForString()) .property("blinded_rsa_signature", codecForString())
.build("RsaBlindedDenominationSignature"); .build("RsaBlindedDenominationSignature");
export const codecForLegacyRsaBlindedDenominationSignature = () =>
buildCodecForObject<LegacyRsaBlindedDenominationSignature>()
.property("cipher", codecForConstNumber(1))
.property("blinded_rsa_signature", codecForString())
.build("LegacyRsaBlindedDenominationSignature");
export class WithdrawResponse { 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 { export interface ExchangeRevealItem {
ev_sig: BlindedDenominationSignature; /**
* The string variant is for the legacy v9 protocol.
*/
ev_sig: BlindedDenominationSignature | string;
} }
export interface ExchangeRevealResponse { export interface ExchangeRevealResponse {
@ -1044,7 +1091,15 @@ export interface BankWithdrawalOperationPostResponse {
transfer_done: boolean; 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 { export interface RsaDenominationPubKey {
cipher: DenomKeyType.Rsa; cipher: DenomKeyType.Rsa;
@ -1061,6 +1116,7 @@ export const codecForDenominationPubKey = () =>
buildCodecForUnion<DenominationPubKey>() buildCodecForUnion<DenominationPubKey>()
.discriminateOn("cipher") .discriminateOn("cipher")
.alternative(1, codecForRsaDenominationPubKey()) .alternative(1, codecForRsaDenominationPubKey())
.alternative(3, codecForLegacyRsaDenominationPubKey())
.build("DenominationPubKey"); .build("DenominationPubKey");
export const codecForRsaDenominationPubKey = () => export const codecForRsaDenominationPubKey = () =>
@ -1069,6 +1125,12 @@ export const codecForRsaDenominationPubKey = () =>
.property("rsa_public_key", codecForString()) .property("rsa_public_key", codecForString())
.build("DenominationPubKey"); .build("DenominationPubKey");
export const codecForLegacyRsaDenominationPubKey = () =>
buildCodecForObject<LegacyRsaDenominationPubKey>()
.property("cipher", codecForConstNumber(3))
.property("rsa_public_key", codecForString())
.build("LegacyRsaDenominationPubKey");
export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> => export const codecForBankWithdrawalOperationPostResponse = (): Codec<BankWithdrawalOperationPostResponse> =>
buildCodecForObject<BankWithdrawalOperationPostResponse>() buildCodecForObject<BankWithdrawalOperationPostResponse>()
.property("transfer_done", codecForBoolean()) .property("transfer_done", codecForBoolean())
@ -1080,10 +1142,13 @@ export type EddsaSignatureString = string;
export type EddsaPublicKeyString = string; export type EddsaPublicKeyString = string;
export type CoinPublicKeyString = string; export type CoinPublicKeyString = string;
export const codecForDenomination = (): Codec<Denomination> => export const codecForDenomination = (): Codec<ExchangeDenomination> =>
buildCodecForObject<Denomination>() buildCodecForObject<ExchangeDenomination>()
.property("value", codecForString()) .property("value", codecForString())
.property("denom_pub", codecForDenominationPubKey()) .property(
"denom_pub",
codecForEither(codecForDenominationPubKey(), codecForString()),
)
.property("fee_withdraw", codecForString()) .property("fee_withdraw", codecForString())
.property("fee_deposit", codecForString()) .property("fee_deposit", codecForString())
.property("fee_refresh", codecForString()) .property("fee_refresh", codecForString())
@ -1101,8 +1166,8 @@ export const codecForAuditorDenomSig = (): Codec<AuditorDenomSig> =>
.property("auditor_sig", codecForString()) .property("auditor_sig", codecForString())
.build("AuditorDenomSig"); .build("AuditorDenomSig");
export const codecForAuditor = (): Codec<Auditor> => export const codecForAuditor = (): Codec<ExchangeAuditor> =>
buildCodecForObject<Auditor>() buildCodecForObject<ExchangeAuditor>()
.property("auditor_pub", codecForString()) .property("auditor_pub", codecForString())
.property("auditor_url", codecForString()) .property("auditor_url", codecForString())
.property("denomination_keys", codecForList(codecForAuditorDenomSig())) .property("denomination_keys", codecForList(codecForAuditorDenomSig()))
@ -1261,7 +1326,7 @@ export const codecForExchangeKeysJson = (): Codec<ExchangeKeysJson> =>
.property("signkeys", codecForList(codecForExchangeSigningKey())) .property("signkeys", codecForList(codecForExchangeSigningKey()))
.property("version", codecForString()) .property("version", codecForString())
.property("reserve_closing_delay", codecForDuration) .property("reserve_closing_delay", codecForDuration)
.build("KeysJson"); .build("ExchangeKeysJson");
export const codecForWireFeesJson = (): Codec<WireFeesJson> => export const codecForWireFeesJson = (): Codec<WireFeesJson> =>
buildCodecForObject<WireFeesJson>() buildCodecForObject<WireFeesJson>()
@ -1327,7 +1392,10 @@ export const codecForRecoupConfirmation = (): Codec<RecoupConfirmation> =>
export const codecForWithdrawResponse = (): Codec<WithdrawResponse> => export const codecForWithdrawResponse = (): Codec<WithdrawResponse> =>
buildCodecForObject<WithdrawResponse>() buildCodecForObject<WithdrawResponse>()
.property("ev_sig", codecForBlindedDenominationSignature()) .property(
"ev_sig",
codecForEither(codecForBlindedDenominationSignature(), codecForString()),
)
.build("WithdrawResponse"); .build("WithdrawResponse");
export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> => export const codecForMerchantPayResponse = (): Codec<MerchantPayResponse> =>
@ -1345,7 +1413,10 @@ export const codecForExchangeMeltResponse = (): Codec<ExchangeMeltResponse> =>
export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> => export const codecForExchangeRevealItem = (): Codec<ExchangeRevealItem> =>
buildCodecForObject<ExchangeRevealItem>() buildCodecForObject<ExchangeRevealItem>()
.property("ev_sig", codecForBlindedDenominationSignature()) .property(
"ev_sig",
codecForEither(codecForBlindedDenominationSignature(), codecForString()),
)
.build("ExchangeRevealItem"); .build("ExchangeRevealItem");
export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> => export const codecForExchangeRevealResponse = (): Codec<ExchangeRevealResponse> =>

View File

@ -49,6 +49,7 @@ import {
codecForContractTerms, codecForContractTerms,
ContractTerms, ContractTerms,
DenominationPubKey, DenominationPubKey,
DenomKeyType,
UnblindedSignature, UnblindedSignature,
} from "./talerTypes.js"; } from "./talerTypes.js";
import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js"; import { OrderShortInfo, codecForOrderShortInfo } from "./transactionsTypes.js";
@ -515,6 +516,7 @@ export interface DepositInfo {
merchantPub: string; merchantPub: string;
feeDeposit: AmountJson; feeDeposit: AmountJson;
wireInfoHash: string; wireInfoHash: string;
denomKeyType: DenomKeyType;
denomPubHash: string; denomPubHash: string;
denomSig: UnblindedSignature; denomSig: UnblindedSignature;
} }

View File

@ -1173,6 +1173,17 @@ export class ExchangeService implements ExchangeServiceInterface {
} }
async runAggregatorOnce() { async runAggregatorOnce() {
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( await runCommand(
this.globalState, this.globalState,
`exchange-${this.name}-aggregator-once`, `exchange-${this.name}-aggregator-once`,
@ -1180,6 +1191,7 @@ export class ExchangeService implements ExchangeServiceInterface {
[...this.timetravelArgArr, "-c", this.configFilename, "-t"], [...this.timetravelArgArr, "-c", this.configFilename, "-t"],
); );
} }
}
async runTransferOnce() { async runTransferOnce() {
await runCommand( await runCommand(

View File

@ -1018,6 +1018,13 @@ const testCli = walletCli.subcommand("testingArgs", "testing", {
help: "Subcommands for 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 testCli
.subcommand("listIntegrationtests", "list-integrationtests") .subcommand("listIntegrationtests", "list-integrationtests")
.action(async (args) => { .action(async (args) => {

View File

@ -52,8 +52,7 @@ export interface TrustInfo {
} }
export interface MerchantInfo { export interface MerchantInfo {
supportsMerchantProtocolV1: boolean; protocolVersionCurrent: number;
supportsMerchantProtocolV2: boolean;
} }
/** /**

View File

@ -392,6 +392,7 @@ export class CryptoApi {
} }
isValidWireAccount( isValidWireAccount(
versionCurrent: number,
paytoUri: string, paytoUri: string,
sig: string, sig: string,
masterPub: string, masterPub: string,
@ -399,6 +400,7 @@ export class CryptoApi {
return this.doRpc<boolean>( return this.doRpc<boolean>(
"isValidWireAccount", "isValidWireAccount",
4, 4,
versionCurrent,
paytoUri, paytoUri,
sig, sig,
masterPub, masterPub,

View File

@ -154,9 +154,10 @@ export class CryptoImplementation {
* reserve. * reserve.
*/ */
createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult { createPlanchet(req: PlanchetCreationRequest): PlanchetCreationResult {
if (req.denomPub.cipher !== 1) { if (
throw Error("unsupported cipher"); req.denomPub.cipher === DenomKeyType.Rsa ||
} req.denomPub.cipher === DenomKeyType.LegacyRsa
) {
const reservePub = decodeCrock(req.reservePub); const reservePub = decodeCrock(req.reservePub);
const reservePriv = decodeCrock(req.reservePriv); const reservePriv = decodeCrock(req.reservePriv);
const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key); const denomPubRsa = decodeCrock(req.denomPub.rsa_public_key);
@ -188,7 +189,7 @@ export class CryptoImplementation {
coinPub: encodeCrock(derivedPlanchet.coinPub), coinPub: encodeCrock(derivedPlanchet.coinPub),
coinValue: req.value, coinValue: req.value,
denomPub: { denomPub: {
cipher: 1, cipher: req.denomPub.cipher,
rsa_public_key: encodeCrock(denomPubRsa), rsa_public_key: encodeCrock(denomPubRsa),
}, },
denomPubHash: encodeCrock(denomPubHash), denomPubHash: encodeCrock(denomPubHash),
@ -197,13 +198,19 @@ export class CryptoImplementation {
coinEvHash: encodeCrock(evHash), coinEvHash: encodeCrock(evHash),
}; };
return planchet; return planchet;
} else {
throw Error("unsupported cipher, unable to create planchet");
}
} }
/** /**
* Create a planchet used for tipping, including the private keys. * Create a planchet used for tipping, including the private keys.
*/ */
createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet { createTipPlanchet(req: DeriveTipRequest): DerivedTipPlanchet {
if (req.denomPub.cipher !== 1) { if (
req.denomPub.cipher !== DenomKeyType.Rsa &&
req.denomPub.cipher !== DenomKeyType.LegacyRsa
) {
throw Error("unsupported cipher"); throw Error("unsupported cipher");
} }
const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex); const fc = setupTipPlanchet(decodeCrock(req.secretSeed), req.planchetIndex);
@ -243,6 +250,19 @@ export class CryptoImplementation {
const coinPriv = decodeCrock(coin.coinPriv); const coinPriv = decodeCrock(coin.coinPriv);
const coinSig = eddsaSign(p, coinPriv); const coinSig = eddsaSign(p, coinPriv);
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 = { const paybackRequest: RecoupRequest = {
coin_blind_key_secret: coin.blindingKey, coin_blind_key_secret: coin.blindingKey,
coin_pub: coin.coinPub, coin_pub: coin.coinPub,
@ -253,6 +273,7 @@ export class CryptoImplementation {
}; };
return paybackRequest; return paybackRequest;
} }
}
/** /**
* Check if a payment signature is valid. * Check if a payment signature is valid.
@ -326,15 +347,31 @@ export class CryptoImplementation {
} }
isValidWireAccount( isValidWireAccount(
versionCurrent: number,
paytoUri: string, paytoUri: string,
sig: string, sig: string,
masterPub: string, masterPub: string,
): boolean { ): boolean {
if (versionCurrent === 10) {
const paytoHash = hash(stringToBytes(paytoUri + "\0")); const paytoHash = hash(stringToBytes(paytoUri + "\0"));
const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS) const p = buildSigPS(TalerSignaturePurpose.MASTER_WIRE_DETAILS)
.put(paytoHash) .put(paytoHash)
.build(); .build();
return eddsaVerify(p, decodeCrock(sig), decodeCrock(masterPub)); 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( isValidContractTermsSignature(
@ -393,7 +430,10 @@ export class CryptoImplementation {
signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission { signDepositPermission(depositInfo: DepositInfo): CoinDepositPermission {
// FIXME: put extensions here if used // FIXME: put extensions here if used
const hExt = new Uint8Array(64); const hExt = new Uint8Array(64);
const d = buildSigPS(TalerSignaturePurpose.WALLET_COIN_DEPOSIT) 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(decodeCrock(depositInfo.contractTermsHash))
.put(hExt) .put(hExt)
.put(decodeCrock(depositInfo.wireInfoHash)) .put(decodeCrock(depositInfo.wireInfoHash))
@ -404,8 +444,25 @@ export class CryptoImplementation {
.put(amountToBuffer(depositInfo.feeDeposit)) .put(amountToBuffer(depositInfo.feeDeposit))
.put(decodeCrock(depositInfo.merchantPub)) .put(decodeCrock(depositInfo.merchantPub))
.build(); .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 coinSig = eddsaSign(d, decodeCrock(depositInfo.coinPriv));
if (depositInfo.denomKeyType === DenomKeyType.Rsa) {
const s: CoinDepositPermission = { const s: CoinDepositPermission = {
coin_pub: depositInfo.coinPub, coin_pub: depositInfo.coinPub,
coin_sig: encodeCrock(coinSig), coin_sig: encodeCrock(coinSig),
@ -418,6 +475,19 @@ export class CryptoImplementation {
}, },
}; };
return s; 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( async deriveRefreshSession(
@ -466,12 +536,14 @@ export class CryptoImplementation {
for (const denomSel of newCoinDenoms) { for (const denomSel of newCoinDenoms) {
for (let i = 0; i < denomSel.count; i++) { for (let i = 0; i < denomSel.count; i++) {
if (denomSel.denomPub.cipher !== 1) { if (denomSel.denomPub.cipher === DenomKeyType.LegacyRsa) {
throw Error("unsupported cipher"); const r = decodeCrock(denomSel.denomPub.rsa_public_key);
} sessionHc.update(r);
} else {
sessionHc.update(hashDenomPub(denomSel.denomPub)); sessionHc.update(hashDenomPub(denomSel.denomPub));
} }
} }
}
sessionHc.update(decodeCrock(meltCoinPub)); sessionHc.update(decodeCrock(meltCoinPub));
sessionHc.update(amountToBuffer(valueWithFee)); sessionHc.update(amountToBuffer(valueWithFee));
@ -508,8 +580,11 @@ export class CryptoImplementation {
blindingFactor = fresh.bks; blindingFactor = fresh.bks;
} }
const pubHash = hash(coinPub); const pubHash = hash(coinPub);
if (denomSel.denomPub.cipher !== 1) { if (
throw Error("unsupported cipher"); 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 denomPub = decodeCrock(denomSel.denomPub.rsa_public_key);
const ev = rsaBlind(pubHash, blindingFactor, denomPub); const ev = rsaBlind(pubHash, blindingFactor, denomPub);

View File

@ -25,7 +25,7 @@ import {
import { import {
AmountJson, AmountJson,
AmountString, AmountString,
Auditor, ExchangeAuditor,
CoinDepositPermission, CoinDepositPermission,
ContractTerms, ContractTerms,
DenominationPubKey, DenominationPubKey,
@ -427,7 +427,7 @@ export interface ExchangeDetailsRecord {
/** /**
* Auditors (partially) auditing the exchange. * Auditors (partially) auditing the exchange.
*/ */
auditors: Auditor[]; auditors: ExchangeAuditor[];
/** /**
* Last observed protocol version. * Last observed protocol version.
@ -1136,6 +1136,7 @@ export interface WalletContractData {
timestamp: Timestamp; timestamp: Timestamp;
wireMethod: string; wireMethod: string;
wireInfoHash: string; wireInfoHash: string;
wireInfoLegacyHash?: string;
maxDepositFee: AmountJson; maxDepositFee: AmountJson;
} }

View File

@ -27,6 +27,7 @@ import {
BackupRefundState, BackupRefundState,
RefreshReason, RefreshReason,
BackupRefreshReason, BackupRefreshReason,
DenomKeyType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
WalletContractData, WalletContractData,
@ -331,7 +332,10 @@ export async function importBackup(
} }
for (const backupDenomination of backupExchangeDetails.denominations) { 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"); throw Error("unsupported cipher");
} }
const denomPubHash = const denomPubHash =

View File

@ -38,14 +38,15 @@ import {
codecForString, codecForString,
codecOptional, codecOptional,
ConfirmPayResultType, ConfirmPayResultType,
DenomKeyType,
durationFromSpec, durationFromSpec,
getTimestampNow, getTimestampNow,
hashDenomPub, hashDenomPub,
HttpStatusCode, HttpStatusCode,
j2s, j2s,
LibtoolVersion,
Logger, Logger,
notEmpty, notEmpty,
NotificationType,
PreparePayResultType, PreparePayResultType,
RecoveryLoadRequest, RecoveryLoadRequest,
RecoveryMergeStrategy, RecoveryMergeStrategy,
@ -167,7 +168,10 @@ async function computeBackupCryptoData(
}; };
for (const backupExchangeDetails of backupContent.exchange_details) { for (const backupExchangeDetails of backupContent.exchange_details) {
for (const backupDenom of backupExchangeDetails.denominations) { 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"); throw Error("unsupported cipher");
} }
for (const backupCoin of backupDenom.coins) { for (const backupCoin of backupDenom.coins) {
@ -184,9 +188,25 @@ async function computeBackupCryptoData(
coinPub, coinPub,
}; };
} }
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[ cryptoData.rsaDenomPubToHash[
backupDenom.denom_pub.rsa_public_key backupDenom.denom_pub.rsa_public_key
] = encodeCrock(hashDenomPub(backupDenom.denom_pub)); ] = encodeCrock(hashDenomPub(backupDenom.denom_pub));
} else {
throw Error("unsupported exchange protocol version");
}
} }
for (const backupReserve of backupExchangeDetails.reserves) { for (const backupReserve of backupExchangeDetails.reserves) {
cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock( cryptoData.reservePrivToPub[backupReserve.reserve_priv] = encodeCrock(

View File

@ -26,6 +26,7 @@ import {
CreateDepositGroupRequest, CreateDepositGroupRequest,
CreateDepositGroupResponse, CreateDepositGroupResponse,
decodeCrock, decodeCrock,
DenomKeyType,
durationFromSpec, durationFromSpec,
getTimestampNow, getTimestampNow,
Logger, Logger,
@ -59,6 +60,8 @@ import {
getCandidatePayCoins, getCandidatePayCoins,
getEffectiveDepositAmount, getEffectiveDepositAmount,
getTotalPaymentCost, getTotalPaymentCost,
hashWire,
hashWireLegacy,
} from "./pay.js"; } from "./pay.js";
/** /**
@ -103,16 +106,6 @@ const codecForDepositSuccess = (): Codec<DepositSuccess> =>
.property("transaction_base_url", codecOptional(codecForString())) .property("transaction_base_url", codecOptional(codecForString()))
.build("DepositSuccess"); .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( async function resetDepositGroupRetry(
ws: InternalWalletState, ws: InternalWalletState,
depositGroupId: string, depositGroupId: string,
@ -211,8 +204,34 @@ async function processDepositGroupImpl(
continue; continue;
} }
const perm = depositPermissions[i]; const perm = depositPermissions[i];
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url); let requestBody: any;
const httpResp = await ws.http.postJson(url.href, { 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), contribution: Amounts.stringify(perm.contribution),
merchant_payto_uri: depositGroup.wire.payto_uri, merchant_payto_uri: depositGroup.wire.payto_uri,
wire_salt: depositGroup.wire.salt, wire_salt: depositGroup.wire.salt,
@ -225,7 +244,10 @@ async function processDepositGroupImpl(
coin_sig: perm.coin_sig, coin_sig: perm.coin_sig,
denom_pub_hash: perm.h_denom, denom_pub_hash: perm.h_denom,
merchant_pub: depositGroup.merchantPub, merchant_pub: depositGroup.merchantPub,
}); };
}
const url = new URL(`coins/${perm.coin_pub}/deposit`, perm.exchange_url);
const httpResp = await ws.http.postJson(url.href, requestBody);
await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess()); await readSuccessResponseJsonOrThrow(httpResp, codecForDepositSuccess());
await ws.db await ws.db
.mktx((x) => ({ depositGroups: x.depositGroups })) .mktx((x) => ({ depositGroups: x.depositGroups }))
@ -358,6 +380,7 @@ export async function createDepositGroup(
const merchantPair = await ws.cryptoApi.createEddsaKeypair(); const merchantPair = await ws.cryptoApi.createEddsaKeypair();
const wireSalt = encodeCrock(getRandomBytes(16)); const wireSalt = encodeCrock(getRandomBytes(16));
const wireHash = hashWire(req.depositPaytoUri, wireSalt); const wireHash = hashWire(req.depositPaytoUri, wireSalt);
const wireHashLegacy = hashWireLegacy(req.depositPaytoUri, wireSalt);
const contractTerms: ContractTerms = { const contractTerms: ContractTerms = {
auditors: [], auditors: [],
exchanges: exchangeInfos, exchanges: exchangeInfos,
@ -371,7 +394,10 @@ export async function createDepositGroup(
nonce: noncePair.pub, nonce: noncePair.pub,
wire_transfer_deadline: timestampRound, wire_transfer_deadline: timestampRound,
order_id: "", order_id: "",
// This is always the v2 wire hash, as we're the "merchant" and support v2.
h_wire: wireHash, h_wire: wireHash,
// Required for older exchanges.
h_wire_legacy: wireHashLegacy,
pay_deadline: timestampAddDuration( pay_deadline: timestampAddDuration(
timestampRound, timestampRound,
durationFromSpec({ hours: 1 }), durationFromSpec({ hours: 1 }),

View File

@ -19,11 +19,11 @@
*/ */
import { import {
Amounts, Amounts,
Auditor, ExchangeAuditor,
canonicalizeBaseUrl, canonicalizeBaseUrl,
codecForExchangeKeysJson, codecForExchangeKeysJson,
codecForExchangeWireJson, codecForExchangeWireJson,
Denomination, ExchangeDenomination,
Duration, Duration,
durationFromSpec, durationFromSpec,
ExchangeSignKeyJson, ExchangeSignKeyJson,
@ -40,6 +40,9 @@ import {
Timestamp, Timestamp,
hashDenomPub, hashDenomPub,
LibtoolVersion, LibtoolVersion,
codecForAny,
DenominationPubKey,
DenomKeyType,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util"; import { decodeCrock, encodeCrock, hash } from "@gnu-taler/taler-util";
import { CryptoApi } from "../crypto/workers/cryptoApi.js"; import { CryptoApi } from "../crypto/workers/cryptoApi.js";
@ -77,11 +80,21 @@ function denominationRecordFromKeys(
exchangeBaseUrl: string, exchangeBaseUrl: string,
exchangeMasterPub: string, exchangeMasterPub: string,
listIssueDate: Timestamp, listIssueDate: Timestamp,
denomIn: Denomination, denomIn: ExchangeDenomination,
): DenominationRecord { ): 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 = { const d: DenominationRecord = {
denomPub: denomIn.denom_pub, denomPub,
denomPubHash, denomPubHash,
exchangeBaseUrl, exchangeBaseUrl,
exchangeMasterPub, exchangeMasterPub,
@ -205,6 +218,7 @@ export async function acceptExchangeTermsOfService(
} }
async function validateWireInfo( async function validateWireInfo(
versionCurrent: number,
wireInfo: ExchangeWireJson, wireInfo: ExchangeWireJson,
masterPublicKey: string, masterPublicKey: string,
cryptoApi: CryptoApi, cryptoApi: CryptoApi,
@ -212,6 +226,7 @@ async function validateWireInfo(
for (const a of wireInfo.accounts) { for (const a of wireInfo.accounts) {
logger.trace("validating exchange acct"); logger.trace("validating exchange acct");
const isValid = await cryptoApi.isValidWireAccount( const isValid = await cryptoApi.isValidWireAccount(
versionCurrent,
a.payto_uri, a.payto_uri,
a.master_sig, a.master_sig,
masterPublicKey, masterPublicKey,
@ -321,7 +336,7 @@ async function provideExchangeRecord(
interface ExchangeKeysDownloadResult { interface ExchangeKeysDownloadResult {
masterPublicKey: string; masterPublicKey: string;
currency: string; currency: string;
auditors: Auditor[]; auditors: ExchangeAuditor[];
currentDenominations: DenominationRecord[]; currentDenominations: DenominationRecord[];
protocolVersion: string; protocolVersion: string;
signingKeys: ExchangeSignKeyJson[]; signingKeys: ExchangeSignKeyJson[];
@ -345,14 +360,14 @@ async function downloadKeysInfo(
const resp = await http.get(keysUrl.href, { const resp = await http.get(keysUrl.href, {
timeout, timeout,
}); });
const exchangeKeysJson = await readSuccessResponseJsonOrThrow( const exchangeKeysJsonUnchecked = await readSuccessResponseJsonOrThrow(
resp, resp,
codecForExchangeKeysJson(), codecForExchangeKeysJson(),
); );
logger.info("received /keys response"); logger.info("received /keys response");
if (exchangeKeysJson.denoms.length === 0) { if (exchangeKeysJsonUnchecked.denoms.length === 0) {
const opErr = makeErrorDetails( const opErr = makeErrorDetails(
TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT, TalerErrorCode.WALLET_EXCHANGE_DENOMINATIONS_INSUFFICIENT,
"exchange doesn't offer any denominations", "exchange doesn't offer any denominations",
@ -363,7 +378,7 @@ async function downloadKeysInfo(
throw new OperationFailedError(opErr); throw new OperationFailedError(opErr);
} }
const protocolVersion = exchangeKeysJson.version; const protocolVersion = exchangeKeysJsonUnchecked.version;
const versionRes = LibtoolVersion.compare( const versionRes = LibtoolVersion.compare(
WALLET_EXCHANGE_PROTOCOL_VERSION, WALLET_EXCHANGE_PROTOCOL_VERSION,
@ -382,29 +397,29 @@ async function downloadKeysInfo(
} }
const currency = Amounts.parseOrThrow( const currency = Amounts.parseOrThrow(
exchangeKeysJson.denoms[0].value, exchangeKeysJsonUnchecked.denoms[0].value,
).currency.toUpperCase(); ).currency.toUpperCase();
return { return {
masterPublicKey: exchangeKeysJson.master_public_key, masterPublicKey: exchangeKeysJsonUnchecked.master_public_key,
currency, currency,
auditors: exchangeKeysJson.auditors, auditors: exchangeKeysJsonUnchecked.auditors,
currentDenominations: exchangeKeysJson.denoms.map((d) => currentDenominations: exchangeKeysJsonUnchecked.denoms.map((d) =>
denominationRecordFromKeys( denominationRecordFromKeys(
baseUrl, baseUrl,
exchangeKeysJson.master_public_key, exchangeKeysJsonUnchecked.master_public_key,
exchangeKeysJson.list_issue_date, exchangeKeysJsonUnchecked.list_issue_date,
d, d,
), ),
), ),
protocolVersion: exchangeKeysJson.version, protocolVersion: exchangeKeysJsonUnchecked.version,
signingKeys: exchangeKeysJson.signkeys, signingKeys: exchangeKeysJsonUnchecked.signkeys,
reserveClosingDelay: exchangeKeysJson.reserve_closing_delay, reserveClosingDelay: exchangeKeysJsonUnchecked.reserve_closing_delay,
expiry: getExpiryTimestamp(resp, { expiry: getExpiryTimestamp(resp, {
minDuration: durationFromSpec({ hours: 1 }), minDuration: durationFromSpec({ hours: 1 }),
}), }),
recoup: exchangeKeysJson.recoup ?? [], recoup: exchangeKeysJsonUnchecked.recoup ?? [],
listIssueDate: exchangeKeysJson.list_issue_date, listIssueDate: exchangeKeysJsonUnchecked.list_issue_date,
}; };
} }
@ -466,7 +481,14 @@ async function updateExchangeFromUrlImpl(
logger.info("validating exchange /wire info"); 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( const wireInfo = await validateWireInfo(
version.current,
wireInfoDownload, wireInfoDownload,
keysInfo.masterPublicKey, keysInfo.masterPublicKey,
ws.cryptoApi, ws.cryptoApi,

View File

@ -52,15 +52,13 @@ export async function getMerchantInfo(
`merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`, `merchant "${canonBaseUrl}" reports protocol ${configResp.version}"`,
); );
const parsedVersion = LibtoolVersion.parseVersion(configResp.version);
if (!parsedVersion) {
throw Error("invalid merchant version");
}
const merchantInfo: MerchantInfo = { const merchantInfo: MerchantInfo = {
supportsMerchantProtocolV1: !!LibtoolVersion.compare( protocolVersionCurrent: parsedVersion.current,
"1:0:0",
configResp.version,
)?.compatible,
supportsMerchantProtocolV2: !!LibtoolVersion.compare(
"2:0:0",
configResp.version,
)?.compatible,
}; };
ws.merchantInfoCache[canonBaseUrl] = merchantInfo; ws.merchantInfoCache[canonBaseUrl] = merchantInfo;

View File

@ -54,6 +54,10 @@ import {
URL, URL,
getDurationRemaining, getDurationRemaining,
HttpStatusCode, HttpStatusCode,
DenomKeyType,
kdf,
stringToBytes,
decodeCrock,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util"; import { encodeCrock, getRandomBytes } from "@gnu-taler/taler-util";
import { import {
@ -108,6 +112,26 @@ import {
*/ */
const logger = new Logger("pay.ts"); 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. * Compute the total cost of a payment to the customer.
* *
@ -669,6 +693,7 @@ export function extractContractData(
timestamp: parsedContractTerms.timestamp, timestamp: parsedContractTerms.timestamp,
wireMethod: parsedContractTerms.wire_method, wireMethod: parsedContractTerms.wire_method,
wireInfoHash: parsedContractTerms.h_wire, wireInfoHash: parsedContractTerms.h_wire,
wireInfoLegacyHash: parsedContractTerms.h_wire_legacy,
maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee), maxDepositFee: Amounts.parseOrThrow(parsedContractTerms.max_fee),
merchant: parsedContractTerms.merchant, merchant: parsedContractTerms.merchant,
products: parsedContractTerms.products, products: parsedContractTerms.products,
@ -882,7 +907,6 @@ async function startDownloadProposal(
claimToken: string | undefined, claimToken: string | undefined,
noncePriv: string | undefined, noncePriv: string | undefined,
): Promise<string> { ): Promise<string> {
const oldProposal = await ws.db const oldProposal = await ws.db
.mktx((x) => ({ proposals: x.proposals })) .mktx((x) => ({ proposals: x.proposals }))
.runReadOnly(async (tx) => { .runReadOnly(async (tx) => {
@ -896,15 +920,19 @@ async function startDownloadProposal(
* If we have already claimed this proposal with the same sessionId * If we have already claimed this proposal with the same sessionId
* nonce and claim token, reuse it. * nonce and claim token, reuse it.
*/ */
if (oldProposal && if (
oldProposal &&
oldProposal.downloadSessionId === sessionId && oldProposal.downloadSessionId === sessionId &&
(!noncePriv || oldProposal.noncePriv === noncePriv) && (!noncePriv || oldProposal.noncePriv === noncePriv) &&
oldProposal.claimToken === claimToken) { oldProposal.claimToken === claimToken
) {
await processDownloadProposal(ws, oldProposal.proposalId); await processDownloadProposal(ws, oldProposal.proposalId);
return 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 proposalId = encodeCrock(getRandomBytes(32));
const proposalRecord: ProposalRecord = { const proposalRecord: ProposalRecord = {
@ -1169,6 +1197,11 @@ async function submitPay(
logger.trace("paying with session ID", sessionId); logger.trace("paying with session ID", sessionId);
const merchantInfo = await ws.merchantOps.getMerchantInfo(
ws,
purchase.download.contractData.merchantBaseUrl,
);
if (!purchase.merchantPaySig) { if (!purchase.merchantPaySig) {
const payUrl = new URL( const payUrl = new URL(
`orders/${purchase.download.contractData.orderId}/pay`, `orders/${purchase.download.contractData.orderId}/pay`,
@ -1568,11 +1601,21 @@ export async function generateDepositPermissions(
for (let i = 0; i < payCoinSel.coinPubs.length; i++) { for (let i = 0; i < payCoinSel.coinPubs.length; i++) {
const { coin, denom } = coinWithDenom[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({ const dp = await ws.cryptoApi.signDepositPermission({
coinPriv: coin.coinPriv, coinPriv: coin.coinPriv,
coinPub: coin.coinPub, coinPub: coin.coinPub,
contractTermsHash: contractData.contractTermsHash, contractTermsHash: contractData.contractTermsHash,
denomPubHash: coin.denomPubHash, denomPubHash: coin.denomPubHash,
denomKeyType: coin.denomPub.cipher,
denomSig: coin.denomSig, denomSig: coin.denomSig,
exchangeBaseUrl: coin.exchangeBaseUrl, exchangeBaseUrl: coin.exchangeBaseUrl,
feeDeposit: denom.feeDeposit, feeDeposit: denom.feeDeposit,
@ -1580,7 +1623,7 @@ export async function generateDepositPermissions(
refundDeadline: contractData.refundDeadline, refundDeadline: contractData.refundDeadline,
spendAmount: payCoinSel.coinContributions[i], spendAmount: payCoinSel.coinContributions[i],
timestamp: contractData.timestamp, timestamp: contractData.timestamp,
wireInfoHash: contractData.wireInfoHash, wireInfoHash,
}); });
depositPermissions.push(dp); depositPermissions.push(dp);
} }
@ -1613,6 +1656,11 @@ export async function confirmPay(
throw Error("proposal is in invalid state"); throw Error("proposal is in invalid state");
} }
const merchantInfo = await ws.merchantOps.getMerchantInfo(
ws,
d.contractData.merchantBaseUrl,
);
const existingPurchase = await ws.db const existingPurchase = await ws.db
.mktx((x) => ({ purchases: x.purchases })) .mktx((x) => ({ purchases: x.purchases }))
.runReadWrite(async (tx) => { .runReadWrite(async (tx) => {

View File

@ -365,7 +365,18 @@ async function refreshMelt(
`coins/${oldCoin.coinPub}/melt`, `coins/${oldCoin.coinPub}/melt`,
oldCoin.exchangeBaseUrl, oldCoin.exchangeBaseUrl,
); );
const 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, coin_pub: oldCoin.coinPub,
confirm_sig: derived.confirmSig, confirm_sig: derived.confirmSig,
denom_pub_hash: oldCoin.denomPubHash, denom_pub_hash: oldCoin.denomPubHash,
@ -373,10 +384,10 @@ async function refreshMelt(
rc: derived.hash, rc: derived.hash,
value_with_fee: Amounts.stringify(derived.meltValueWithFee), value_with_fee: Amounts.stringify(derived.meltValueWithFee),
}; };
logger.trace(`melt request for coin:`, meltReq); }
const resp = await ws.runSequentialized([EXCHANGE_COINS_LOCK], async () => { 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), timeout: getRefreshRequestTimeout(refreshGroup),
}); });
}); });
@ -604,15 +615,26 @@ async function refreshReveal(
continue; continue;
} }
const pc = derived.planchetsForGammas[norevealIndex][newCoinIndex]; 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"); throw Error("cipher unsupported");
} }
const evSig = reveal.ev_sigs[newCoinIndex].ev_sig; 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"); throw Error("unsupported cipher");
} }
const denomSigRsa = await ws.cryptoApi.rsaUnblind( const denomSigRsa = await ws.cryptoApi.rsaUnblind(
evSig.blinded_rsa_signature, rsaSig,
pc.blindingKey, pc.blindingKey,
denom.denomPub.rsa_public_key, denom.denomPub.rsa_public_key,
); );

View File

@ -314,13 +314,13 @@ async function processTipImpl(
let blindedSigs: BlindedDenominationSignature[] = []; let blindedSigs: BlindedDenominationSignature[] = [];
if (merchantInfo.supportsMerchantProtocolV2) { if (merchantInfo.protocolVersionCurrent === 2) {
const response = await readSuccessResponseJsonOrThrow( const response = await readSuccessResponseJsonOrThrow(
merchantResp, merchantResp,
codecForMerchantTipResponseV2(), codecForMerchantTipResponseV2(),
); );
blindedSigs = response.blind_sigs.map((x) => x.blind_sig); blindedSigs = response.blind_sigs.map((x) => x.blind_sig);
} else if (merchantInfo.supportsMerchantProtocolV1) { } else if (merchantInfo.protocolVersionCurrent === 1) {
const response = await readSuccessResponseJsonOrThrow( const response = await readSuccessResponseJsonOrThrow(
merchantResp, merchantResp,
codecForMerchantTipResponseV1(), codecForMerchantTipResponseV1(),
@ -347,11 +347,17 @@ async function processTipImpl(
const planchet = planchets[i]; const planchet = planchets[i];
checkLogicInvariant(!!planchet); checkLogicInvariant(!!planchet);
if (denom.denomPub.cipher !== DenomKeyType.Rsa) { if (
denom.denomPub.cipher !== DenomKeyType.Rsa &&
denom.denomPub.cipher !== DenomKeyType.LegacyRsa
) {
throw Error("unsupported cipher"); throw Error("unsupported cipher");
} }
if (blindedSig.cipher !== DenomKeyType.Rsa) { if (
blindedSig.cipher !== DenomKeyType.Rsa &&
blindedSig.cipher !== DenomKeyType.LegacyRsa
) {
throw Error("unsupported cipher"); throw Error("unsupported cipher");
} }

View File

@ -42,6 +42,7 @@ import {
VersionMatchResult, VersionMatchResult,
DenomKeyType, DenomKeyType,
LibtoolVersion, LibtoolVersion,
UnblindedSignature,
} from "@gnu-taler/taler-util"; } from "@gnu-taler/taler-util";
import { import {
CoinRecord, CoinRecord,
@ -591,12 +592,28 @@ async function processPlanchetVerifyAndStoreCoin(
const { planchet, exchangeBaseUrl } = d; const { planchet, exchangeBaseUrl } = d;
const planchetDenomPub = planchet.denomPub; const planchetDenomPub = planchet.denomPub;
if (planchetDenomPub.cipher !== DenomKeyType.Rsa) { if (
throw Error("cipher not supported"); planchetDenomPub.cipher !== DenomKeyType.Rsa &&
planchetDenomPub.cipher !== DenomKeyType.LegacyRsa
) {
throw Error(`cipher (${planchetDenomPub.cipher}) not supported`);
} }
const evSig = resp.ev_sig; let evSig = resp.ev_sig;
if (evSig.cipher !== DenomKeyType.Rsa) { 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"); throw Error("unsupported cipher");
} }
@ -633,6 +650,19 @@ async function processPlanchetVerifyAndStoreCoin(
return; 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 = { const coin: CoinRecord = {
blindingKey: planchet.blindingKey, blindingKey: planchet.blindingKey,
coinPriv: planchet.coinPriv, coinPriv: planchet.coinPriv,
@ -640,10 +670,7 @@ async function processPlanchetVerifyAndStoreCoin(
currentAmount: planchet.coinValue, currentAmount: planchet.coinValue,
denomPub: planchet.denomPub, denomPub: planchet.denomPub,
denomPubHash: planchet.denomPubHash, denomPubHash: planchet.denomPubHash,
denomSig: { denomSig,
cipher: DenomKeyType.Rsa,
rsa_signature: denomSigRsa,
},
coinEvHash: planchet.coinEvHash, coinEvHash: planchet.coinEvHash,
exchangeBaseUrl: exchangeBaseUrl, exchangeBaseUrl: exchangeBaseUrl,
status: CoinStatus.Fresh, status: CoinStatus.Fresh,

View File

@ -23,7 +23,12 @@
/** /**
* Imports. * 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"; import { strcmp, Logger } from "@gnu-taler/taler-util";
const logger = new Logger("coinSelection.ts"); const logger = new Logger("coinSelection.ts");
@ -215,10 +220,21 @@ function denomPubCmp(
} else if (p1.cipher > p2.cipher) { } else if (p1.cipher > p2.cipher) {
return +1; return +1;
} }
if (p1.cipher !== 1 || p2.cipher !== 1) { if (
throw Error("unsupported cipher"); 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); return strcmp(p1.rsa_public_key, p2.rsa_public_key);
} else {
throw Error("unsupported cipher");
}
} }
/** /**

View File

@ -19,14 +19,14 @@
* *
* Uses libtool's current:revision:age versioning. * 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. * Protocol version spoken with the merchant.
* *
* Uses libtool's current:revision:age versioning. * 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. * 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. * This is only a temporary measure.
*/ */
export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "3"; export const WALLET_CACHE_BREAKER_CLIENT_VERSION = "4";

View File

@ -390,7 +390,7 @@ async function runTaskLoop(
} catch (e) { } catch (e) {
if (e instanceof OperationFailedAndReportedError) { if (e instanceof OperationFailedAndReportedError) {
logger.warn("operation processed resulted in reported error"); 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 { } else {
logger.error("Uncaught exception", e); logger.error("Uncaught exception", e);
ws.notify({ ws.notify({
@ -985,6 +985,8 @@ export async function handleCoreApiRequest(
e instanceof OperationFailedError || e instanceof OperationFailedError ||
e instanceof OperationFailedAndReportedError e instanceof OperationFailedAndReportedError
) { ) {
logger.error("Caught operation failed error");
logger.trace((e as any).stack);
return { return {
type: "error", type: "error",
operation, operation,