This is a chunky PR, but can mercifully be thought of as 4 distinct sections:
The exposed complement-crypto API for key verification:
internal/api/client.go
internal/api/verification.go
The JS implementation of the API:
internal/api/js/*
The Rust implementation of the API:
internal/api/rust/*
The test which uses the API:
tests/verification_test.go
There are a number of TODOs here to keep the size of this smaller:
the RPC client impl is not implemented
JS clients cannot request verification yet, only rust can.
These will be fixed in another PR.
The most important bit to review is the API, as that is what test writers will interact with. This is described in more detail below:
internal/api/client.go: this is the entry point for verification. It's a function that determines if you are the sender or receiver for the key verification process. Either way, you get back a channel of VerificationStages. Using channels is nice because you can select over multiple channels at once, meaning the test doesn't need to have multiple goroutines (one for sender, one for receiver), the select effectively serialises changes. Using select also has the benefit that it is trivial to implement a timeout function via case <-time.After(5 * time.Second): - this means we will fail the test if there is no state change in 5 seconds, rather than wedging indefinitely.
internal/api/verification.go: this has a lot of interfaces and enums and structs, but in essence it just defines the states that the verification state machine can be in. Every client must implement these states. By having an interface per state, it means we can guide the caller into performing valid state transitions i.e you cannot go from a cancelled state to a ready state, there physically isn't a function you can call. In contrast, the ready state has a Cancel() function which will transition to the cancelled state.
VerificationContainer: To aid client implementations there is a VerificationContainer which can store all relevant verification functionality and data, and it satisfies all state interfaces. Unfortunately, Go is not expressive enough to be able to say "convert VerificationContainer to thing which only satisfies this interface". Whilst you can trivially assign to an interface, the duck typing combined with the concrete type implementing all states means the first type assertion will always match. To work around this, enums and concrete structs for each stage are added, which is what func (c *VerificationContainer) Stage(stageEnum VerificationStageEnum) VerificationStage does. This maps the container to a smaller struct which only implements the desired interface.
This PR implements own user verification (cross-signing) for sender=rust, receiver=js. It forms part of https://github.com/matrix-org/complement-crypto/issues/72
This is a chunky PR, but can mercifully be thought of as 4 distinct sections:
internal/api/client.go
internal/api/verification.go
internal/api/js/*
internal/api/rust/*
tests/verification_test.go
There are a number of TODOs here to keep the size of this smaller:
These will be fixed in another PR.
The most important bit to review is the API, as that is what test writers will interact with. This is described in more detail below:
internal/api/client.go
: this is the entry point for verification. It's a function that determines if you are the sender or receiver for the key verification process. Either way, you get back a channel ofVerificationStage
s. Using channels is nice because you canselect
over multiple channels at once, meaning the test doesn't need to have multiple goroutines (one for sender, one for receiver), theselect
effectively serialises changes. Usingselect
also has the benefit that it is trivial to implement a timeout function viacase <-time.After(5 * time.Second):
- this means we will fail the test if there is no state change in 5 seconds, rather than wedging indefinitely.internal/api/verification.go
: this has a lot of interfaces and enums and structs, but in essence it just defines the states that the verification state machine can be in. Every client must implement these states. By having an interface per state, it means we can guide the caller into performing valid state transitions i.e you cannot go from a cancelled state to a ready state, there physically isn't a function you can call. In contrast, the ready state has aCancel()
function which will transition to the cancelled state.VerificationContainer
: To aid client implementations there is aVerificationContainer
which can store all relevant verification functionality and data, and it satisfies all state interfaces. Unfortunately, Go is not expressive enough to be able to say "convertVerificationContainer
to thing which only satisfies this interface". Whilst you can trivially assign to an interface, the duck typing combined with the concrete type implementing all states means the first type assertion will always match. To work around this, enums and concrete structs for each stage are added, which is whatfunc (c *VerificationContainer) Stage(stageEnum VerificationStageEnum) VerificationStage
does. This maps the container to a smaller struct which only implements the desired interface.