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

feat: Session API #13

Merged
merged 35 commits into from
Jan 29, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
78e867f
feat: ApplicationInfoV2 with string version
igor-sirotin Jan 16, 2025
39f6ec7
feat: KeycardContextV2
igor-sirotin Jan 16, 2025
61a8834
feat: rpc service
igor-sirotin Jan 16, 2025
98723f9
feat: http server for the session API
igor-sirotin Jan 16, 2025
79bc43f
feat: ensure api mutual exclusivity
igor-sirotin Jan 16, 2025
c8e2670
chore: go mod tidy
igor-sirotin Jan 16, 2025
cf33a32
fix: marshal c api errors
igor-sirotin Jan 17, 2025
a31e16c
feat: added `NoAvailablePairingSlots` state
igor-sirotin Jan 17, 2025
f36505d
fix: fixed Initialize requset, added validation, added FactoryReset
igor-sirotin Jan 17, 2025
008f4f9
fix: pin and puk operations
igor-sirotin Jan 17, 2025
dc72e10
fix: LoadMnemonic, rename GenerateMnemonic
igor-sirotin Jan 17, 2025
a937ce7
feat: pull states instead of pnp subscribe
igor-sirotin Jan 19, 2025
0a11cf2
chore: go mod tidy
igor-sirotin Jan 20, 2025
ce293c7
feat: metadata, change puk, refactor
igor-sirotin Jan 20, 2025
65a2c97
feat: added versionRaw for simpler comparison
igor-sirotin Jan 20, 2025
2925ae2
feat: ExportLoginKeys, ExportRecoverKeys
igor-sirotin Jan 20, 2025
8615a1d
feat: Authorized state, fixed stopping service
igor-sirotin Jan 20, 2025
a3804cc
WIP
igor-sirotin Jan 21, 2025
854f0c4
Loop V3
igor-sirotin Jan 21, 2025
48e01ec
fix: cleanup states and better monitoring
igor-sirotin Jan 22, 2025
2e4cf60
fix: active reader state polling
igor-sirotin Jan 22, 2025
b414330
chore: simplify force scan and auth status update
igor-sirotin Jan 22, 2025
6b9e1bd
fix: graceful stop
igor-sirotin Jan 22, 2025
60a0f69
feat: api
igor-sirotin Jan 22, 2025
ac69548
feat: ApplicationStatus type
igor-sirotin Jan 23, 2025
75b1c9e
docs: description of the Session API
igor-sirotin Jan 23, 2025
bcf0945
docs: update state struct link
igor-sirotin Jan 23, 2025
e4da931
chore: jetbrains idea run configuration
igor-sirotin Jan 24, 2025
5302740
chore: minor cleanup
igor-sirotin Jan 24, 2025
39b3adc
fix: use exclusive connection, graceful disconnect and release (#15)
igor-sirotin Jan 24, 2025
d7f7473
feat: StoreMetadata
igor-sirotin Jan 24, 2025
0a354c9
fix: linter
igor-sirotin Jan 24, 2025
5408297
fix: SimulateError issues
igor-sirotin Jan 29, 2025
5fa2c5f
feat: improved pre-command status checks
igor-sirotin Jan 29, 2025
c128567
fix: add missing simulatedSelectAppletError
igor-sirotin Jan 29, 2025
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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.vscode
.idea
.idea/*
!.idea/runConfigurations
/keycard
/build
/status-keycard-go
/api/http-client.private.env.json
12 changes: 12 additions & 0 deletions .idea/runConfigurations/status_keycard_server.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# status-keycard-go

This library provides a higher level Keycard API for Status app. It is currently only used in [status-desktop](https://github.com/status-im/status-desktop/).

There are 2 types of API provided.

## Flow API

Each keycard command is executed in a single _flow_. A flow roughly looks like this:
1. List available readers
2. Look for a keycard
3. Set up a connection
4. Execute the command
5. Close the connection

If client interaction is required at any stage (e.g. insert a card, input a PIN), the flow is "paused" and signals to the client. The client should manually continue the flow when the required action was performed. This basically drives the UI right from `status-keycard-go` library.

> [!NOTE]
> status-desktop doesn't use this API anymore. Consider switching to Session API.

## Session API

The main problem with Flow API is that it does not signal certain changes, e.g. "reader disconnected" and "card removed". Session API addresses this issue.

The journey begins with `Start` endpoint. When the keycard service is started, it monitors all connected readers and cards. This allows to track the state of reader+card and notify the client on any change. As soon as a keycard is found, a "connection" (pair and open secure channel) is established automatically and will be reused until `Stop` is called or the keycard is removed.

In the `Ready`/`Authorized` states client can execute wanted commands, each as a separate endpoint.

Check out the detailed usage in ./api/README.md

12 changes: 12 additions & 0 deletions api/Authorize.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @name Authorize
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.Authorize",
"params": [
{
"pin": "654321"
}
]
}
12 changes: 12 additions & 0 deletions api/ChangePIN.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @name ChangePIN
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.ChangePIN",
"params": [
{
"newPIN": "654321"
}
]
}
12 changes: 12 additions & 0 deletions api/ChangePUK.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @name ChangePUK
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.ChangePUK",
"params": [
{
"newPuk": "654321654322"
}
]
}
8 changes: 8 additions & 0 deletions api/ExportLoginKeys.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @name ExportLoginKeys
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.ExportLoginKeys",
"params": []
}
8 changes: 8 additions & 0 deletions api/ExportRecoverKeys.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @name ExportRecoverKeys
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.ExportRecoverKeys",
"params": []
}
8 changes: 8 additions & 0 deletions api/FactoryReset.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @name FactoryReset
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.FactoryReset",
"params": []
}
12 changes: 12 additions & 0 deletions api/GenerateMnemonic.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @name Initialize
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.GenerateMnemonic",
"params": [
{
"length": 12
}
]
}
8 changes: 8 additions & 0 deletions api/GetMetadata.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @name GetMetadata
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.GetMetadata",
"params": []
}
8 changes: 8 additions & 0 deletions api/GetStatus.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# @name GetStatus
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.GetStatus",
"params": []
}
13 changes: 13 additions & 0 deletions api/Initialize.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# @name Initialize
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.Initialize",
"params": [
{
"pin": "654321",
"puk": "654321654321"
}
]
}
12 changes: 12 additions & 0 deletions api/LoadMnemonic.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @name LoadMnemonic
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.LoadMnemonic",
"params": [
{
"mnemonic": "tiger worth relief food limb glad recycle similar wreck work region uncover"
}
]
}
190 changes: 190 additions & 0 deletions api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
> [!NOTE]
> This guide is not comprehensive and relies on your wisdom and intelligence.
> Only _session_ API is considered. For the _flow_ API please check out the source code.

# Description

This directory contains `*.http` request for each available endpoint in the Session API.

# Usage

Session API uses JSON-RPC protocol. All commands are available at `keycard` service. Here is an example:
```json
{
"id": "1",
"method": "keycard.Authorize",
"params": [
{
"pin": "654321"
}
]
}
```

There are 2 ways to access the API.

## HTTP

This way is easier to use for testing and debugging.

1. Run the server:
```shell
go run ./cmd/status-keycard-server/main.go --address=localhost:12346
```

2. Connect to signals websocket at `ws://localhost:12346/signals`

3. Send requests to `http://localhost:12346/rpc`

## C bindings

This is the way to integrate `status-keycard-go` library, e.g. how `status-desktop` uses it.

To subscribe to signals, set a callback function with `setSignalEventCallback`.

For the RPC server, there are 2 methods provided:
1. `InitializeRPC` - must be called once at the start of the application, before making any RPC calls.
2. `CallRPC` - call with a single JSON string argument according to the JSON-RPC protocol. Returns a single JSON string response.


# Setup

1. Connect to signals
For the session API, the only emitted signal is `status-changed`.
It provides current status of the session and information about connected keycard.
2. Call `Start`
From this moment, until `Stop` is called, the keycard service will take care of watching readers/cards and keeping a secure "connection" with a keycard.
Provide `StorageFilePath`, e.g. `~/pairings.json`. This file will be used to store pairing keys for all paired keycards.
3. If planning to execute any authorization-required commands, call `Authorize`
4. Monitor state of the session, execute any needed commands.
NOTE: Some of the keycard commands can only be executed in `ready` or `authorized` state.
5. Call `Stop`

# Simulation

Because it is difficult (perhaps nearly impossible) to implement proper simulation of a keycard,
this library provides a way to simulate certain errors, which is not simple/possible to achieve with hardware.

Check [`SimulateErrro`](#simulateerror) method for details

# API

## Signals

Signals follow the structure described here: https://github.com/keycard-tech/status-keycard-go/blob/b1e1f7f0bf534269a5c18fcd31649d2056b13e5b/signal/signals.go#L27-L31

The only signal type used in Session API is `status-changed`. For event structure, check out [Status](#status)

## Service endpoints

These endpoints are related to the `status-keycard-go` library itself:

## `Start`

Starts the monitoring of readers and cards.

The monitoring starts with _detect_ mode.
In this mode it checks all connected readers for a smart cards. Monitoring supports events (like reader connection and card insertion) to happen even after calling `Start`.

As soon as a reader with a Keycard is found, the monitoring switches to _watch_ mode:
- Only the reader with the keycard is watched. If the keycard is removed, or the reader is disconnected, the monitoring goes back to _detect_ mode.
- Any new connected readers, or inserted smart cards on other readers, are ignored.

[//]: # (TODO: Diagram)

## `Stop`

Stops the monitoring.

## `SimulateError`

Marks that certain error should be simulated.

For the `simulated-not-a-keycard` error, `InstanceUID` argument must be provided. Only keycards with such `InstanceUID` will be treated as not keycards.
Other errors are applied no matter of the `InstanceUID` value.

`SimulateError` can also be called before `Start`, e.g. to simulate `simulated-no-pcsc` error, as this one can only happen during `Start` operation.

Use `SimulateError` method with one of the supported simulation errors: https://github.com/keycard-tech/status-keycard-go/blob/a3804cc8848a93a277895e508dd7c423f1f8338c/internal/keycard_context_v2_state.go#L55-L62

## `GetStatus`

Returns current status of the session.

In most scenarios `status-changed` signal should be used to get status. Yet in some debugging circumstances this method can be handy to get the latest status.

## Status

Here is the structure of the status: https://github.com/keycard-tech/status-keycard-go/blob/a3804cc8848a93a277895e508dd7c423f1f8338c/internal/keycard_context_v2_state.go#L30-L35

The main field is `state`

### State

Check the source code for the list of possible states and their description.
https://github.com/keycard-tech/status-keycard-go/blob/75b1c9eac08de708724e2ee36909764fcafa858e/internal/keycard_context_v2_state.go#L9-L71

## Commands

Apart from the service endpoints listed above, all other endpoints represent the actual [Keycard API](https://keycard.tech/docs/apdu).

Most of the commands have to be executed in `ready` or `authorized` states. Service will return a readable error if the keycard is not in the proper state for the command.

Please check out the Keycard documentation for more details.

## Examples

The examples are presented in a "you'll get it" form.
`<-` represents a reception of `status-changed` signal.

### Initialize a new Keycard

```go
Start("~/pairings.json")
<- "waiting-for-reader"
// connect a reader
<- "waiting-for-card"
// insert a keycard
<- "connecting-card"
<- "empty-keycard", AppInfo: { InstanceUID: "abcd" }, AppStatus: null
Initialize(pin: "123456", puk: "123456123456")
<- "ready", Appinfo: ..., AppStatus: { remainingAttemptsPIN: 3, remainingAttemptsPUK: 5, ... }
Authorize(pin: "123456")
<- "autorized", AppInfo: ..., AppStatus ...
ExportLoginKeys()
```

### Unblock a Keycard

```go
Start("~/pairings.json")
<- "waiting-for-reader"
// connect a reader with a keycard
<- "connecting-card"
<- "blocked-pin", AppInfo: { InstanceUID: "abcd" }, AppStatus: { remainingAttemptsPIN: 0, remainingAttemptsPUK: 5, ... }
UnblockPIN(puk: "123456123456")
<- "authorized", AppInfo: ..., AppStatus: { remainingAttemptsPIN: 3, remainingAttemptsPUK: 5, ... }
```

### Factory reset a completely blocked Keycard

```go
Start("~/pairings.json")
<- "waiting-for-reader"
// connect a reader with a keycard
<- "connecting-card"
<- "blocked-puk", AppInfo: { InstanceUID: "abcd" }, AppStatus: { remainingAttemptsPIN: 0, remainingAttemptsPUK: 0, ... }
FactoryReset()
<- "factory-resetting"
<- "empty-keycard"
```

# Implementation decisions

1. Monitoring detect mode utilizes [`\\?PnP?\Notification`](https://blog.apdu.fr/posts/2024/08/improved-scardgetstatuschange-for-pnpnotification-special-reader/) feature to detect new connected readers without any CPU load.
2. Monitoring watch mode could use a blocking call to `GetStatusChange`, but this did not work on Linux (Ubuntu), although worked on MacOS.
So instead there is a loop that checks the state of the reader each 500ms.
3. JSON-RPC was chosen for 2 reasons:
- to expose API to HTTP for testing/debugging
- to simplify the adding new methods to the API
gRPC was also considered, but this would require more work on `status-desktop`.
3 changes: 3 additions & 0 deletions api/Signals.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# @name Signals
WEBSOCKET ws://{{address}}/signals
Content-Type: application/json
12 changes: 12 additions & 0 deletions api/SimulateError.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# @name SimulateError
POST {{address}}/rpc

{
"id": "{{$random.uuid}}",
"method": "keycard.SimulateError",
"params": [
{
"error": "simulated-no-pcsc"
}
]
}
Loading