Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/workshop blockaid #596

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/sample/src/components/CalView/CalSettingsDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ export function CalSettingsDrawer({ onClose }: CalSettingsDrawerProps) {
};
const labelSelector: Record<string, string> = {
url: "CAL URL",
web3checksUrl: "Web3checks URL",
mode: "Mode",
branch: "Branch reference",
};

const onSettingsUpdate = useCallback(() => {
const { url, mode, branch } = values;
const { url, web3checksUrl, mode, branch } = values;
const isMode = (test: unknown): test is "prod" | "test" =>
test === "prod" || test === "test";
const isBranch = (test: unknown): test is "main" | "next" | "demo" =>
Expand All @@ -46,6 +47,10 @@ export function CalSettingsDrawer({ onClose }: CalSettingsDrawerProps) {
console.error("Invalid CAL URL", url);
return;
}
if (!web3checksUrl || typeof web3checksUrl !== "string" || !web3checksUrl.startsWith("http")) {
console.error("Invalid CAL URL", url);
return;
}

if (!mode || !isMode(mode)) {
console.error("Invalid mode", mode);
Expand All @@ -59,6 +64,7 @@ export function CalSettingsDrawer({ onClose }: CalSettingsDrawerProps) {

const newSettings: ContextModuleCalConfig = {
url,
web3checksUrl,
mode,
branch,
};
Expand Down
22 changes: 17 additions & 5 deletions apps/sample/src/components/SignerEthView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,11 +103,23 @@ export const SignerEthView: React.FC<{ sessionId: string }> = ({
if (!signer) {
throw new Error("Signer not initialized");
}
return signer.signTransaction(
derivationPath,
ethers.Transaction.from(transaction),
{ domain: recipientDomain },
);
try {
const parsedTransaction = JSON.parse(transaction);
if ("from" in parsedTransaction) {
delete parsedTransaction.from;
}
return signer.signTransaction(
derivationPath,
ethers.Transaction.from(parsedTransaction),
{ domain: recipientDomain },
);
} catch (error) {
return signer.signTransaction(
derivationPath,
ethers.Transaction.from(transaction),
{ domain: recipientDomain },
);
}
},
initialValues: {
derivationPath: "44'/60'/0'/0/0",
Expand Down
1 change: 1 addition & 0 deletions apps/sample/src/providers/SignerEthProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const initialState: SignerEthContextType = {
signer: null,
calConfig: {
url: "https://crypto-assets-service.api.ledger.com/v1",
web3checksUrl: "https://api.blockaid.io/v0/ledger/transaction/scan",
mode: "prod",
branch: "main",
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { type Apdu } from "@api/apdu/model/Apdu";
import { ApduBuilder, type ApduBuilderArgs } from "@api/apdu/utils/ApduBuilder";
import { ApduParser } from "@api/apdu/utils/ApduParser";
import {
type CommandResult,
CommandResultFactory,
} from "@api/command/model/CommandResult";
import {
type CommandErrors,
isCommandErrorCode,
} from "@api/command/utils/CommandErrors";
import { CommandUtils } from "@api/command/utils/CommandUtils";
import { GlobalCommandErrorHandler } from "@api/command/utils/GlobalCommandError";
import { type ApduResponse } from "@api/device-session/ApduResponse";
import { DeviceExchangeError } from "@api/Error";
import { type Command, type CommandErrorArgs } from "@api/types";

export type LoadCertificateArgs = {
readonly keyUsage: number;
readonly certificate: Uint8Array;
};

export type LoadCertificateErrorCodes =
| "422F"
| "4230"
| "4231"
| "4232"
| "4233"
| "4234"
| "4235"
| "4236"
| "4237"
| "4238"
| "4239"
| "422D"
| "3301"
| "422E"
| "5720"
| "4118"
| "FFFF";

const LOAD_CERTIFICATE_ERRORS: CommandErrors<LoadCertificateErrorCodes> = {
"422F": { message: "Incorrect structure type" },
"4230": { message: "Incorrect certificate version" },
"4231": { message: "Incorrect certificate validity" },
"4232": { message: "Incorrect certificate validity index" },
"4233": { message: "Unknown signer key ID" },
"4234": { message: "Unknown signature algorithm" },
"4235": { message: "Unknown public key ID" },
"4236": { message: "Unknown public key usage" },
"4237": { message: "Incorrect elliptic curve ID" },
"4238": {
message: "Incorrect signature algorithm associated to the public key",
},
"4239": { message: "Unknown target device" },
"422D": { message: "Unknown certificate tag" },
"3301": { message: "Failed to hash data" },
"422E": {
message: "expected_key_usage doesn't match certificate key usage",
},
"5720": { message: "Failed to verify signature" },
"4118": {
message: "trusted_name buffer is too small to contain the trusted name",
},
FFFF: { message: "Cryptography-related error" },
};

export class LoadCertificateCommandError extends DeviceExchangeError<LoadCertificateErrorCodes> {
constructor({
message,
errorCode,
}: CommandErrorArgs<LoadCertificateErrorCodes>) {
super({ tag: "ProvidePkiCertificateCommandError", message, errorCode });
}
}

export type LoadCertificateCommandResult = CommandResult<
void,
LoadCertificateErrorCodes
>;

/**
* The command to load a certificate on the device.
*/
export class LoadCertificateCommand
implements Command<void, LoadCertificateArgs, LoadCertificateErrorCodes>
{
readonly args: LoadCertificateArgs;
readonly triggersDisconnection = false;

constructor(args: LoadCertificateArgs) {
this.args = args;
}

getApdu(): Apdu {
const providePkiApduArgs: ApduBuilderArgs = {
cla: 0xb0,
ins: 0x06,
p1: this.args.keyUsage,
p2: 0x00,
};
return new ApduBuilder(providePkiApduArgs)
.addBufferToData(this.args.certificate)
.build();
}

parseResponse(apduResponse: ApduResponse): LoadCertificateCommandResult {
if (CommandUtils.isSuccessResponse(apduResponse)) {
return CommandResultFactory({
data: undefined,
});
}
const parser = new ApduParser(apduResponse);
const errorCode = parser.encodeToHexaString(apduResponse.statusCode);
if (isCommandErrorCode(errorCode, LOAD_CERTIFICATE_ERRORS)) {
return CommandResultFactory({
error: new LoadCertificateCommandError({
...LOAD_CERTIFICATE_ERRORS[errorCode],
errorCode,
}),
});
}
return CommandResultFactory({
error: GlobalCommandErrorHandler.handle(apduResponse),
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,13 +119,17 @@ export class DeviceSession {
options.triggersDisconnection,
);

return errorOrResponse.ifRight((response: ApduResponse) => {
if (CommandUtils.isLockedDeviceResponse(response)) {
this.updateDeviceStatus(DeviceStatus.LOCKED);
} else {
return errorOrResponse
.ifRight((response: ApduResponse) => {
if (CommandUtils.isLockedDeviceResponse(response)) {
this.updateDeviceStatus(DeviceStatus.LOCKED);
} else {
this.updateDeviceStatus(DeviceStatus.CONNECTED);
}
})
.ifLeft(() => {
this.updateDeviceStatus(DeviceStatus.CONNECTED);
}
});
});
}

async sendCommand<Response, Args, ErrorStatusCodes>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ export class DisconnectUseCase {
this._transportService
.getTransport(transportIdentifier)
.toEither(
new TransportNotSupportedError(new Error("Unknown transport")),
new TransportNotSupportedError(
new Error(
`Unknown transport with identifier: ${transportIdentifier}`,
),
),
),
);

Expand Down
4 changes: 4 additions & 0 deletions packages/signer/context-module/src/ContextModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,15 @@ import {
} from "./shared/model/TransactionContext";
import { type TypedDataClearSignContext } from "./shared/model/TypedDataClearSignContext";
import { type TypedDataContext } from "./shared/model/TypedDataContext";
import { type Web3CheckContext } from "./web3-check/domain/web3CheckTypes";

export interface ContextModule {
getContext(field: TransactionFieldContext): Promise<ClearSignContext>;
getContexts(transaction: TransactionContext): Promise<ClearSignContext[]>;
getTypedDataFilters(
typedData: TypedDataContext,
): Promise<TypedDataClearSignContext>;
getWeb3Checks(
transactionContext: Web3CheckContext,
): Promise<ClearSignContext | null>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ describe("ContextModuleBuilder", () => {
url: "https://crypto-assets-service.api.ledger.com/v1",
mode: "prod",
branch: "main",
web3checksUrl: "todo",
};
it("should return a default context module", () => {
const contextModuleBuilder = new ContextModuleBuilder();
Expand Down
15 changes: 15 additions & 0 deletions packages/signer/context-module/src/ContextModuleBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import {
} from "./config/model/ContextModuleConfig";
import { type ContextLoader } from "./shared/domain/ContextLoader";
import { type TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader";
import { type Web3CheckContextLoader } from "./web3-check/domain/Web3CheckContextLoader";
import { type ContextModule } from "./ContextModule";
import { DefaultContextModule } from "./DefaultContextModule";

const DEFAULT_CAL_URL = "https://crypto-assets-service.api.ledger.com/v1";
const DEFAULT_WEB3_CHECKS_URL =
"https://crypto-assets-service.api.ledger.com/v1";

export const DEFAULT_CONFIG: ContextModuleConfig = {
cal: {
url: DEFAULT_CAL_URL,
web3checksUrl: DEFAULT_WEB3_CHECKS_URL,
mode: "prod",
branch: "main",
},
Expand Down Expand Up @@ -57,6 +61,17 @@ export class ContextModuleBuilder {
return this;
}

/**
* Replace the default loader for web3 checks
*
* @param loader loader to use for web3 checks
* @returns this
*/
addWeb3CheckLoader(loader: Web3CheckContextLoader) {
this.config.customWeb3CheckLoader = loader;
return this;
}

/**
* Add a custom CAL configuration
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ describe("DefaultContextModule", () => {
url: "https://crypto-assets-service.api.ledger.com/v1",
mode: "prod",
branch: "main",
web3checksUrl: "https://api.blockaid.io/v0/ledger/transaction/scan",
},
};

Expand Down
30 changes: 30 additions & 0 deletions packages/signer/context-module/src/DefaultContextModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,26 @@ import { type TransactionContextLoader } from "./transaction/domain/TransactionC
import { type TrustedNameContextLoader } from "./trusted-name/domain/TrustedNameContextLoader";
import { typedDataTypes } from "./typed-data/di/typedDataTypes";
import type { TypedDataContextLoader } from "./typed-data/domain/TypedDataContextLoader";
import { web3CheckTypes } from "./web3-check/di/web3CheckTypes";
import { type Web3CheckContextLoader } from "./web3-check/domain/Web3CheckContextLoader";
import { type Web3CheckContext } from "./web3-check/domain/web3CheckTypes";
import { type ContextModule } from "./ContextModule";
import { makeContainer } from "./di";

export class DefaultContextModule implements ContextModule {
private _container: Container;
private _loaders: ContextLoader[];
private _typedDataLoader: TypedDataContextLoader;
private _web3CheckLoader: Web3CheckContextLoader;

constructor(args: ContextModuleConfig) {
this._container = makeContainer({ config: args });
this._loaders = args.defaultLoaders ? this._getDefaultLoaders() : [];
this._loaders.push(...args.customLoaders);
this._typedDataLoader =
args.customTypedDataLoader ?? this._getDefaultTypedDataLoader();
this._web3CheckLoader =
args.customWeb3CheckLoader ?? this._getWeb3CheckLoader();
}

private _getDefaultLoaders(): ContextLoader[] {
Expand All @@ -63,6 +69,12 @@ export class DefaultContextModule implements ContextModule {
);
}

private _getWeb3CheckLoader(): Web3CheckContextLoader {
return this._container.get<Web3CheckContextLoader>(
web3CheckTypes.Web3CheckContextLoader,
);
}

public async getContexts(
transaction: TransactionContext,
): Promise<ClearSignContext[]> {
Expand Down Expand Up @@ -91,4 +103,22 @@ export class DefaultContextModule implements ContextModule {
): Promise<TypedDataClearSignContext> {
return this._typedDataLoader.load(typedData);
}

public async getWeb3Checks(
transactionContext: Web3CheckContext,
): Promise<ClearSignContext | null> {
const web3Checks = await this._web3CheckLoader.load(transactionContext);

if (web3Checks.isLeft()) {
return null;
} else {
const web3ChecksValue = web3Checks.unsafeCoerce();
// add Nano PKI fetch here should looks like =>
// const web3CheckCertificate = await this._pkiCertificateLoader.fetchCertificate(...)
return {
type: ClearSignContextType.WEB3_CHECK,
payload: web3ChecksValue.descriptor,
};
}
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { type ContextLoader } from "@/shared/domain/ContextLoader";
import { type TypedDataContextLoader } from "@/typed-data/domain/TypedDataContextLoader";
import { type Web3CheckContextLoader } from "@/web3-check/domain/Web3CheckContextLoader";

export type ContextModuleCalMode = "prod" | "test";
export type ContextModuleCalBranch = "next" | "main" | "demo";

export type ContextModuleCalConfig = {
url: string;
web3checksUrl: string;
mode: ContextModuleCalMode;
branch: ContextModuleCalBranch;
};
Expand All @@ -15,4 +17,5 @@ export type ContextModuleConfig = {
defaultLoaders: boolean;
customLoaders: ContextLoader[];
customTypedDataLoader?: TypedDataContextLoader;
customWeb3CheckLoader?: Web3CheckContextLoader;
};
Loading
Loading