Skip to content

feat: rcan v2 - #27

Draft
rklaehn wants to merge 50 commits into
mainfrom
rklaehn/stable-serde
Draft

feat: rcan v2#27
rklaehn wants to merge 50 commits into
mainfrom
rklaehn/stable-serde

Conversation

@rklaehn

@rklaehn rklaehn commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Description

Adds rcan v2.

Delegation is an opaque delegation that can contain both v1 and v2. If Delegation contains a V1 delegation, encode will produce bytes that are readable by rcan v1. reading a v1 delegation via decode_any<C> requires C. Once read, a v1 Delegation can be serialized and deserialized without needing C, but the serialization format is not compatible with v1.

TypedDelegation<C> is the above but you know that the bytes are compatible with C.

V1Compat<C> serializes and deserializes exactly the same as 0.4 rcan::RCan<C> for all serializers including weird ones (ron).

Authorizer is mostly unchanged, but it works with chains of Delegation or TypedDelegation<C> that can each contain v1 or v2. It has fns for both opaque and typed delegation chains, that use the same underlying code.

Breaking Changes

Basically everything, but the concepts of the API remain roughly the same. We could have less breakage by renaming TypedDelegation<C> to Rcan<C>, but it needs to be a version increase because serialization via serde changed.

Notes & open questions

Q: Is the typed or the opaque variant more frequently used? Depending on which we could name things, e.g. Delegation and TypedDelegation<C> or Delegation<C> and OpaqueDelegation or something?

Q: Is the V1Compat<C> pulling its weight? What it does is allowing us to read v1 Rcan<C> via serde, but we could accomplish the same by just importing rcan 0.4.1 in addition to rcan 0.5 and having some glue. I guess it is nice to not need 2 versions of the same crate, also we don't need a dep to ctserde. But really I don't know...

Q: Should we keep calling a delegation a "RCan"?

Q: TypedDelegation<C> can in principle keep both the bytes and the deserialized C, to stop us from deserializing multiple times. The question is: is this worth it?

Q: Currently Delegation serialize uses textual format for human readable serializers. This looks nice, but requires dealing with the quirks of the various serializers. Alternatives would be 1. just declare that only encode/decode and postcard is stable. 2. use base64(postcard) as string for the human readable formats.

Todo

  • publish rcan 0.4.1 that has the new dalek so we have a dalek-compatible v1 that we can import
  • wire this up in svc and try it out

@rklaehn
rklaehn requested a review from matheus23 August 6, 2026 11:25
rklaehn added 3 commits August 6, 2026 14:28
we got rcan two times, so just the name is ambiguous
Comment thread src/lib.rs

@matheus23 matheus23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorizer is mostly unchanged, but it works with chains of Delegation or TypedDelegation<C> that can each contain v1 or v2. It has fns for both opaque and typed delegation chains, that use the same underlying code.

I think this is more complicated than it needs to be: We never used Authorizer so far - it's fine if it only handles the current version.

TypedDelegation<C> is the above but you know that the bytes are compatible with C.

I'd like to scope the PR down a bit. Do we strictly need this? I don't think we need to introduce this type. It just means users (and agents) will more often have to choose whether to use TypedDelegation or Delegation in their code.
It's totally fine if users just deserialize a Delegation over the wire and then check it using Authorizer later.
(Compatibility code that never uses Authorizer is a different story but needs to be handled differently anyways.)

Q: Is the V1Compat<C> pulling its weight? What it does is allowing us to read v1 Rcan<C> via serde, but we could accomplish the same by just importing rcan 0.4.1 in addition to rcan 0.5 and having some glue. I guess it is nice to not need 2 versions of the same crate, also we don't need a dep to ctserde. But really I don't know...

I'm not sure. If we can skip it, that's great! I guess that would need a careful look at svc to see if we can just introduce new backhaul versions etc. without breaking things.

Q: Should we keep calling a delegation a "RCan"?

We should never actually use this capitalization... It's RCAN, matching UCAN.
That said, no I think calling the crate "rcan" and calling delegations "Delegation" seems totally fine to me.

Q: Currently Delegation serialize uses textual format for human readable serializers. This looks nice, but requires dealing with the quirks of the various serializers. Alternatives would be 1. just declare that only encode/decode and postcard is stable. 2. use base64(postcard) as string for the human readable formats.

Or hex(postcard). Yeah not sure.
There are cases like query parameters where encoding a delegation into "human-readable" strings is desirable. But tbh, for those cases you want to encode a whole delegation chain anyways, and probably rather encode as base64url(deflate(postcard(delegation_chain))).
So yeah - not super necessary IMO.

@rklaehn

rklaehn commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

I'm not sure. If we can skip it, that's great! I guess that would need a careful look at svc to see if we can just introduce new backhaul versions etc. without breaking things.

Skipping it means we have to have a dep on both rcan 0.4.1 or 0.5.0 and whatever the version with the new format is. That is a bit weird, but also kind of honest. We import the last published version of the old format to read the old format.

It also means that conversion to and from Rcan<C> to Delegation<C> needs to live in a helper fn and can not be in the crate itself, because the crate itself depending on a former version of itself is just a bit too weird, even if perfectly legal.

@rklaehn

rklaehn commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Or hex(postcard). Yeah not sure.
There are cases like query parameters where encoding a delegation into "human-readable" strings is desirable. But tbh, for those cases you want to encode a whole delegation chain anyways, and probably rather encode as base64url(deflate(postcard(delegation_chain))).
So yeah - not super necessary IMO.

This is a good reason to have a DelegationChain like in the other PR after all... Delegations chains contain redundancy with the keys, so deflating it by default would help with size. But then do we really care about some small multiple of 32 bytes?

@matheus23

matheus23 commented Aug 7, 2026

Copy link
Copy Markdown
Member

It also means that conversion to and from Rcan<C> to Delegation<C> needs to live in a helper fn and can not be in the crate itself, because the crate itself depending on a former version of itself is just a bit too weird, even if perfectly legal.

My suspicion is we won't need these conversions.
E.g. the main integration point we care about is svc-relay's auth. There we look at the token, if it's v1, we do a completely different authorization check than with v2. The two auth systems effectively are separate.

And then updating the other systems (net-diagnostics, etc.) can work similarly I think.

(And I probably wouldn't want to update the web token auth stuff, that one doesn't benefit from delegations at all, so we probably don't want to do that yet)

@matheus23 matheus23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry I'm having a hard time formulating actionable feedback on this PR.

All I can say is I feel like this introduces so much stuff... There are now 6 manual implementations of serde traits in lib.rs and 5 more in v1.rs, there are a bunch of variants of check_invocation_from for both with and without time and typed or untyped.
The whole thing has now become quite big and I'm not sure if everything in here is pulling its weight...

Perhaps I'd much rather be hand-held and introduce things one-by-one and understand why we need each thing really well. Maybe I'm just getting lapped here not sure.

FWIW it just means my review will take more time at the end of the day.

Comment thread src/lib.rs Outdated
@rklaehn

rklaehn commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

I'm sorry I'm having a hard time formulating actionable feedback on this PR.

All I can say is I feel like this introduces so much stuff... There are now 6 manual implementations of serde traits in lib.rs and 5 more in v1.rs, there are a bunch of variants of check_invocation_from for both with and without time and typed or untyped. The whole thing has now become quite big and I'm not sure if everything in here is pulling its weight...

Perhaps I'd much rather be hand-held and introduce things one-by-one and understand why we need each thing really well. Maybe I'm just getting lapped here not sure.

FWIW it just means my review will take more time at the end of the day.

The manual implementations are needed I think to replicate v1 behaviour, and are a bit more verbose than necessary so we avoid the dependency on serdect. We can also just use serdect everywhere to save a bit of verbosity. I just checked - it also has slice

I am not sure if a line by line review is the best thing to do right now. This is still draft state, so maybe just look at the API using cargo doc and see what works and what doesn't?

E.g. I think we could save quite a lot of surface area if we didn't have the OpaqueDelegation. But then I think we are going to need it, so I'd rather not remove it.

@rklaehn
rklaehn requested a review from matheus23 August 13, 2026 15:21
Required some more serde plumbing to avoid the serde-bytes dep.

@matheus23 matheus23 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked into this a bit which gave me some ideas for how we might make this easier, which I had to try out.
What do you think of this version: https://github.com/n0-computer/rcan/tree/matheus23/reduced ?

It's way more minimal though, but it's enough for all intents and purposes in smartpipe.

Comment thread src/lib.rs Outdated
Comment on lines +174 to +177
struct Signed {
payload: Payload,
signature: Signature,
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do OpaqueDelegation need to be a wrapper around Signed for a good reason right now?
I find the name Signed to be fairly bad. But also - I don't think we need it. We could just remove the indirection between OpaqueDelegation and Signed, no?

Comment thread src/lib.rs Outdated
///
/// Make sure to verify that the `invoker` signed and authenticated the
/// message containing the `capability`.
pub fn check_opaque_invocation_from<C: Capability>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you had a need to check opaque delegation chains?
My experiments in smartpipe have shown that I don't really need that. I just parse into Delegation<SmartpipeCapability> on requests directly. And I don't really see why we wouldn't parse into the correct type immediately on requests?

Comment thread src/lib.rs Outdated
Comment on lines +327 to +330
/// The issuer of the delegation.
pub fn issuer(&self) -> &VerifyingKey {
self.opaque.issuer()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a ton of repetition here.
We have Delegation::issuer, Payload::issuer and OpaqueDelegation::issuer.
If we ever add a ::nonce() we need to make sure we don't forget about one of these.

What do you think of making Payload private and instead just expose stuff on the delegation? And then at least Delegation and OpaqueDelegation can just reach directly into &self.payload.issuer, but that's fine because it's an internal struct in the same module.

The reduced 'rework' commit had reverted Delegation serde to
Vec::<u8>::serialize(), which cbor/ciborium encodes as an integer array
rather than a byte string. Restore serializer.serialize_bytes so the CBOR
form matches the byte-string wire format. Drop the incorrect cbor_array
assertion (both array and byte-string forms round-trip) and remove the
stale is_err() check that only held before serialize_bytes.
Reduce the number of structs and indirection in RCAN v2
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants