Contagious Traits

I'm currently implementing a Secret trait that marks all types that directly carry secrets (indirections like references and pointers are excluded). I extended the trait solvers in a straightforward way with its semantics: Primitive secret types have an explicit implementation. From there, a composite type is marked Secret if any of its components is marked Secret.

However, to abstract this behaviour and to reduce compiler changes specific to secrecy, this concept could be generalized to, e.g., "contagious traits". These could behave like an existential counterpart to auto traits. Unlike auto traits, same as for the Secret trait described above, there must be explicit implementations as a base case. From there, types are contagious w.r.t. this kind of trait and composite types inherit the marker if any of its components is marked. Negative implementations could function as a prohibition of implementation (to my understanding, this is the case for regular traits). However, at least for the Secret trait I'd need this concept for, there must not be a way to opt out, thus negative implementations for infected types would need to yield compile-time errors.

I understand this is a very niche requirement and it may not be worth generalizing, but maybe there are other use-cases for this concept. I'm also not yet aware of all soundness concerns regarding the trait solvers, but my specific implementation right now does not deal with negative implementations at all, as the trait forbids explicit implementations and they are generated from a struct keyword (may be subject to change).

Thank you in advance for further use-cases or comments on the abstraction!

Can you invert the polarity? auto trait NoSecrets {} then impl !NoSecrets for SecretHolder {}?

2 Likes

I think the normal solution to this problem (seen in functional programming languages, and sometimes used in Rust by types like Pin) is to allow secrets to only be accessed via wrapper types that prevent information leaking out.

Something like this would be a start:

struct Secret<T>(T);

impl Secret<T> {
    fn map<U>(self, f: impl Fn(T) ->U) -> Secret<U> {
         Secret((f)(self.0))
    }
}

but unfortunately it isn't 100% secure (because despite not being able to capture a mutable reference, a Fn is still able to use interior mutability in order to exfiltrate the secrets). Assuming you're just doing this to catch errors in code written by legitimate programmers and not in an attempt to interact with malicious code, you could probably work around this by writing an unsafe Fn variant whose safety condition required the function to be pure (i.e. no I/O and no use of interior mutability – if this is cryptographic code, possibly also requiring no sidechannels).

You would of course need a way to actually do something with the resulting value after computation, but that could be an unsafe fn for getting the value out of the Secret, asserting that the value has been operated on to an extent that it's no longer secret (or that it's being stored somewhere which is capable of holding secrets safely).

1 Like

@scottmcm To an extent yes, but as there are no negative trait bounds, this is not optimal. It also feels a bit awkward design-wise.

@ais523 A wrapper does not really solve the problem, as this is merely an aid to outline information-flow semantics. Even assuming an extensive newtype approach (which I am in fact also using), this does not address concerns like secret-aware memory allocation or isolation of secret-dependent code. I’m fairly convinced I do need a trait that works the way I described. I’m uncertain how much I should share for academic reasons (with which I disagree, but oh well…), but these are concepts I’m currently implementing in rustc: https://es.cs.rptu.de/publications/datarsg/HaSc25.pdf Focussing on the hardly elaborated upon secret memory semantics - think of secret memory regions for secrets and when declassified, they need to be copied to public memory. Much like Freeze is used to decide what goes to read-only memory, one of the purposes of this trait is to decide what goes to secret memory.

Thank you both for your input!

I remember reading a blog earlier this year that showed that adding additional auto traits was expensive wrt compile time. This was especially bad for those that didn't stop at pointer indirection. I can't seem to locate that blog post currently (search engines are being unhelpful).

If that is still the case I would be opposed to adding such a niche auto/existential trait as this. There are far more universally useful traits that we would want for systems programming, such as Move, and the whole MetaSized project.

So one thing I want to see early is the compile time cost to code that isn't making use of this new feature you are proposing. Do the benchmarks and show that the cost is negligible (I don't personally think this is worth it at all unless the cost is basically zero).

3 Likes

What are you planning on doing with this trait, BTW? What needs to be bound on it?

What functions only work on things holding secrets? (Feels more obvious that there are things that are only allowed on things not holding secrets.)

4 Likes

@Vorpal Interesting… Well, I’d go as far as saying no such trait, no reasonably ergonomic secure crypto in Rust (and yes, basically all crypto code in most other languages can be shown to be broken by the compiler, and pretty much none takes things like DIT into account).

Almost everything I implemented so far library-wise is in its own crate - this trait is one of like two or three exceptions. I feel like if compile-time is an issue, it could also be banished to the separate crate, the crate is made opt-in, and the related lang_item logic in the compiler could be made conditional. Not pretty, but also not too bad, this feels like a solvable problem. I’ll try this later.

If your conclusion from this is that auto (and contagious) traits should be kept to an absolute minimum, fair enough. Maybe this should not be abstracted then. Then again, I do wonder to which extent they can be made lazy (i.e., only computed when actually queried in some way). Maybe if there are other use-cases that can also be restricted to optional crates, there’s some value to this.

@scottmcm Well, conditional selection. If you conditionally select a public value by a secret condition, the non-constant-time nature of the selected value can leak the secret condition when processed subsequently. This goes both for direct timing (e.g., division) and indirect timing (e.g., array indexing). Of course you could declassify immediately after selection and unintentionally leak both values depending on the nature of the data, but there’s only so much you can protect against.

Surprisingly, I haven’t yet found much use for a bound on !Secret, because the type system enforces very strict rules on them regardless of their Secret marker. You can’t divide them at all, you can’t evaluate them in a non-secret operation except for explicitly declassifying first, you can’t print them, etc. A secrecy-check ensures that only functions with a secret qualifier can receive and operate on types that implement Secret. I have relaxed this to interact with general traits like Add and I might relax things further, but this is still all in drafting.

this post? adding implicit auto-trait bounds is hard :3

3 Likes

Thanks! That was the one I had in mind.

@aszs Thanks for the reference! This makes a lot of sense. Luckily, both traits that are currently interesting to me (Secret and ValueType) do stop at indirection.

@Vorpal Despite the fact Secret stops at indirection, I started trying to make the trait optional (and, at the same time, removing my keyword that defines it in favour of explicit implementations) and while not finished, it looks very much doable. So regardless of the performance cost, I think this is a fair solution.

Anyway, I hope the idea of the Sectet trait is somewhat clear now. Any comments on whether this concept should be generalized?

I’ve been approaching trait objects, as they seem to be the most critical mean to “hide” marker traits (my general secrecy check currently runs in PostAnalysis, so opaque types are revealed; the unsizing coercion check has to happen after monomorphization, of course). As a starter, I prevented coercion from types that implement Secret to types that do not. This means that, right now, secrets can be used as trait objects only if the base trait implies Secret.

At this time, secondary traits must be auto traits, which Secret clearly isn’t. Yet, like auto trait, “contagious traits” would not be able to implement trait methods or the like, forcing them to be marker traits. Thus, I see three options for extending the support for trait objects in this context:

  1. Add a dedicated check for Secret to the trait classification.
  2. Land “contagious traits” and support them alongside auto traits.
  3. Abstract the classification further to a broader kind of marker traits. I’m not positive what the exact requirements are at this time.