Skip to content

Commit

Permalink
Merge pull request #140 from LedgerHQ/fbe/unified_flows
Browse files Browse the repository at this point in the history
New unified flows
  • Loading branch information
fbeutin-ledger authored Aug 22, 2023
2 parents 2c48669 + abd01d8 commit 2ce752b
Show file tree
Hide file tree
Showing 40 changed files with 1,256 additions and 882 deletions.
261 changes: 261 additions & 0 deletions src/apdu_parser.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
/*****************************************************************************
* Ledger
* (c) 2023 Ledger
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*****************************************************************************/

#include "os.h"
#include "ux.h"
#include "os_io_seproxyhal.h"
#include "init.h"
#include "io.h"
#include "menu.h"
#include "globals.h"
#include "commands.h"
#include "command_dispatcher.h"
#include "apdu_offsets.h"
#include "swap_errors.h"
#include "apdu_parser.h"

// Keep as global the received fields of the command
// We need to remember them in case we receive a split command
typedef struct apdu_s {
bool expecting_more;
uint8_t instruction;
uint8_t rate;
uint8_t subcommand;
uint16_t data_length;
// We could have put the recontructed apdu buffer here but it would increase the RAM usage by
// 512 bytes which is a lot on NANOS
// Instead the recontructed apdu buffer is G_swap_ctx.raw_transaction
// It is unionized with the decoded protobuf transaction requests
// Pro: less memory usage
// Cons: use cautiously and only during the command PROCESS_TRANSACTION_RESPONSE_COMMAND
// The split reception is only made for this command anyway
} apdu_t;
apdu_t G_received_apdu;

// Dedicated function for instruction checking as it's self contained
static uint16_t check_instruction(uint8_t instruction, uint8_t subcommand) {
bool check_subcommand_context = false;
bool allowed_during_waiting_for_signing = false;
int check_current_state = -1;

switch (instruction) {
case GET_VERSION_COMMAND:
// We ignore the current context for this command as it doesn't modify anything
check_subcommand_context = false;
// No strict dependancy on the current state as long as it is not a protected state
// (WAITING_USER_VALIDATION and WAITING_SIGNING)
check_current_state = -1;
break;
case START_NEW_TRANSACTION_COMMAND:
// We can always restart a new transaction
allowed_during_waiting_for_signing = true;
check_subcommand_context = false;
check_current_state = -1;
break;
case SET_PARTNER_KEY_COMMAND:
check_current_state = WAITING_TRANSACTION;
check_subcommand_context = true;
break;
case CHECK_PARTNER_COMMAND:
check_current_state = PROVIDER_SET;
check_subcommand_context = true;
break;
case PROCESS_TRANSACTION_RESPONSE_COMMAND:
check_current_state = PROVIDER_CHECKED;
check_subcommand_context = true;
break;
case CHECK_TRANSACTION_SIGNATURE_COMMAND:
check_current_state = TRANSACTION_RECEIVED;
check_subcommand_context = true;
break;
case CHECK_PAYOUT_ADDRESS:
check_current_state = SIGNATURE_CHECKED;
check_subcommand_context = true;
break;
case CHECK_REFUND_ADDRESS:
check_current_state = TO_ADDR_CHECKED;
check_subcommand_context = true;
break;
case START_SIGNING_TRANSACTION:
check_current_state = WAITING_SIGNING;
allowed_during_waiting_for_signing = true;
check_subcommand_context = true;
break;
default:
PRINTF("Received unknown subcommand %d\n", subcommand);
return INVALID_INSTRUCTION;
}

if (G_swap_ctx.state == WAITING_USER_VALIDATION) {
PRINTF("Refuse all APDUs during UI display\n");
return UNEXPECTED_INSTRUCTION;
}

if (!allowed_during_waiting_for_signing && G_swap_ctx.state == WAITING_SIGNING) {
PRINTF("Received subcommand %d, not allowed during WAITING_SIGNING state\n", subcommand);
return UNEXPECTED_INSTRUCTION;
}

if (check_subcommand_context && subcommand != G_swap_ctx.subcommand) {
PRINTF("Received subcommand %d, current flow is %d\n", subcommand, G_swap_ctx.subcommand);
return UNEXPECTED_INSTRUCTION;
}

if (check_current_state != -1 && G_swap_ctx.state != check_current_state) {
PRINTF("Received subcommand %d requiring state %d, but current state is %d\n",
subcommand,
check_current_state,
G_swap_ctx.state);
return UNEXPECTED_INSTRUCTION;
}

return 0;
}

// Return 0 if we can proceed with this APDU, return a status code if we can not
uint16_t apdu_parser(uint8_t *apdu, size_t apdu_length, command_t *command) {
if (apdu_length < OFFSET_CDATA) {
PRINTF("Error: malformed APDU, length is too short %d\n", apdu_length);
return MALFORMED_APDU;
}

uint8_t data_length = apdu[OFFSET_LC];
if (data_length != apdu_length - OFFSET_CDATA) {
PRINTF("Error: malformed APDU, recv %d bytes, claiming %d header bytes and %d data bytes\n",
apdu_length,
OFFSET_CDATA,
data_length);
return MALFORMED_APDU;
}

if (apdu[OFFSET_CLA] != CLA) {
PRINTF("Error: invalid CLA %d\n", apdu[OFFSET_CLA]);
return CLASS_NOT_SUPPORTED;
}

// Get rate from P1
uint8_t rate = apdu[OFFSET_P1];
if (rate != FIXED && rate != FLOATING) {
PRINTF("Incorrect P1 %d\n", rate);
return WRONG_P1;
}

// Extract subcommand from P2
uint8_t subcommand = apdu[OFFSET_P2] & SUBCOMMAND_MASK;
if (subcommand != SWAP && subcommand != SELL && subcommand != FUND && subcommand != SWAP_NG &&
subcommand != SELL_NG && subcommand != FUND_NG) {
PRINTF("Incorrect subcommand %d\n", subcommand);
return WRONG_P2_SUBCOMMAND;
}

// Get instruction and ensure it makes sense in the current context
uint8_t instruction = apdu[OFFSET_INS];
uint16_t err = check_instruction(instruction, subcommand);
if (err != 0) {
return err;
}

// Extract extension from P2
uint8_t extension = apdu[OFFSET_P2] & EXTENSION_MASK;
if ((extension & ~(P2_NONE | P2_MORE | P2_EXTEND)) != 0) {
PRINTF("Incorrect extension %d\n", extension);
return WRONG_P2_EXTENSION;
}

bool is_first_data_chunk = !(extension & P2_EXTEND);
bool is_last_data_chunk = !(extension & P2_MORE);
bool is_whole_apdu = is_first_data_chunk && is_last_data_chunk;
// Split reception is only for NG flows
if (subcommand != SWAP_NG && subcommand != SELL_NG && subcommand != FUND_NG && !is_whole_apdu) {
PRINTF("Extension %d refused, only allowed for unified flows\n", extension);
return WRONG_P2_EXTENSION;
}
// Split reception is only for PROCESS_TRANSACTION_RESPONSE_COMMAND
if (instruction != PROCESS_TRANSACTION_RESPONSE_COMMAND && !is_whole_apdu) {
PRINTF("Extension %d refused, only allowed for PROCESS_TRANSACTION_RESPONSE instruction\n",
extension);
return WRONG_P2_EXTENSION;
}

if (is_first_data_chunk) {
G_received_apdu.instruction = instruction;
G_received_apdu.rate = rate;
G_received_apdu.subcommand = subcommand;
G_received_apdu.data_length = data_length;
if (!is_last_data_chunk) {
// Use the raw_transaction buffer as temporary storage.
// It's unionized with the decoded transaction but we are currently handling
// PROCESS_TRANSACTION_RESPONSE_COMMAND instruction (checked previously).
// After this command is done it will contain the decoded PB data, don't use it anymore
memcpy(G_swap_ctx.raw_transaction, apdu + OFFSET_CDATA, data_length);
G_received_apdu.expecting_more = true;
}
} else {
// The command received claims to extend a previously received one. Check if it's expected
if (!G_received_apdu.expecting_more) {
PRINTF("Previous command did not indicate a followup\n");
return INVALID_P2_EXTENSION;
}
// Also ensure they are the same kind
if (G_received_apdu.instruction != instruction || G_received_apdu.rate != rate ||
G_received_apdu.subcommand != subcommand) {
PRINTF("Refusing to extend a different apdu, exp (%d, %d, %d), recv (%d, %d, %d)\n",
G_received_apdu.instruction,
G_received_apdu.rate,
G_received_apdu.subcommand,
instruction,
rate,
subcommand);
return INVALID_P2_EXTENSION;
}
if (G_received_apdu.data_length + data_length > sizeof(G_swap_ctx.raw_transaction)) {
PRINTF("Reception buffer size %d is not sufficient to receive more data (%d + %d)\n",
sizeof(G_swap_ctx.raw_transaction),
G_received_apdu.data_length,
data_length);
return INVALID_P2_EXTENSION;
}
// Extend the already received buffer
memcpy(G_swap_ctx.raw_transaction + G_received_apdu.data_length,
apdu + OFFSET_CDATA,
data_length);
G_received_apdu.data_length += data_length;
}

if (!is_last_data_chunk) {
// Reply a blank success to indicate that we await the followup part
// Do NOT update any kind of internal state machine, we have not validated what we have
// received
PRINTF("Split APDU successfully initiated, size %d\n", G_received_apdu.data_length);
return SUCCESS;
} else {
// The APDU is valid and complete, signal caller that it can proceed
command->ins = G_received_apdu.instruction;
command->rate = G_received_apdu.rate;
command->subcommand = G_received_apdu.subcommand;
command->data.size = G_received_apdu.data_length;
if (is_whole_apdu) {
// No split has taken place, data is still in the APDU reception buffer
command->data.bytes = apdu + OFFSET_CDATA;
} else {
// Split has taken place, data is in the split buffer
PRINTF("Split APDU successfully recreated, size %d\n", G_received_apdu.data_length);
command->data.bytes = G_swap_ctx.raw_transaction;
}
return 0;
}
}
5 changes: 5 additions & 0 deletions src/apdu_parser.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#pragma once

#include "commands.h"

uint16_t apdu_parser(uint8_t *apdu, size_t apdu_length, command_t *command);
31 changes: 26 additions & 5 deletions src/buffer.c
Original file line number Diff line number Diff line change
@@ -1,21 +1,42 @@
#include "buffer.h"
#include "os.h"

// Parse a buffer at a given offset to read a buf_t, the offset is incremented accordingly
// param[in] <in_buffer> the total buffer to parse
// param[in] <in_size> the total size of the buffer to parse
// param[out] <out> the buf_t read from in_buffer at offset, can by 0 sized
// param[in/out] <offset> the current offset at wich we are in <in_buffer>
bool parse_to_sized_buffer(uint8_t *in_buffer, uint16_t in_size, buf_t *out, uint16_t *offset) {
if (*offset + 1 > in_size) {
bool parse_to_sized_buffer(uint8_t *in_buffer,
uint16_t in_size,
uint8_t size_of_lenght_field,
buf_t *out,
uint16_t *offset) {
if (*offset + size_of_lenght_field > in_size) {
// We can't even read the size
PRINTF("Failed to read the header sized %d, only %d bytes available\n",
size_of_lenght_field,
in_size);
return false;
}

// Read the size
out->size = in_buffer[*offset];
++*offset;
if (size_of_lenght_field == 1) {
out->size = in_buffer[*offset];
} else if (size_of_lenght_field == 2) {
out->size = U2BE(in_buffer, *offset);
} else if (size_of_lenght_field == 4) {
out->size = U4BE(in_buffer, *offset);
} else {
PRINTF("Unable to read a %d sized header\n", size_of_lenght_field);
return false;
}
*offset += size_of_lenght_field;

if (*offset + out->size > in_size) {
// Not enough bytes to read buffer size
PRINTF("Not enough remaining bytes to read. Total %d, offset %d, claims %d bytes\n",
in_size,
*offset,
out->size);
return false;
}

Expand Down
6 changes: 5 additions & 1 deletion src/buffer.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ typedef struct buf_s {
uint16_t size;
} buf_t;

bool parse_to_sized_buffer(uint8_t *in_buffer, uint16_t in_size, buf_t *out, uint16_t *offset);
bool parse_to_sized_buffer(uint8_t *in_buffer,
uint16_t in_size,
uint8_t size_of_lenght_field,
buf_t *out,
uint16_t *offset);
Loading

0 comments on commit 2ce752b

Please sign in to comment.