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

libstore: generalize signature scheme and introduce remote signing #9076

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

RaitoBezarius
Copy link
Member

@RaitoBezarius RaitoBezarius commented Sep 30, 2023

Motivation

As discussed in NixOS/infra#272.

We would like to hide the signing key on another machine to onboard more folks to help on infrastructure of NixOS.

Context

As moving to HSM/PKCS#11 is out of scope in the current timeline we are operating in, we are proposing a simpler solution, albeit very flexible one, which is remote signing technology for Nix.

For this, we need to generalize the signature scheme that assumes we always possess the private key material.

Now, signature schemes use an abstract Signer object that can be instantiated anywhere
and passed around in the hierarchy for signing relevant objects.

The old local signing scheme is now implemented as the LocalSigner.

The abstract Signer always require a local PublicKey to make verification cheap.

In addition, we introduce another signer: the RemoteSigner, adhering to a "Nix remote signing API",
which is a need for the operations of the NixOS.org infrastructure community, to move the signing key location
in a separate piece of infrastructure with increased scrutiny and security.

The Nix remote signing API has 2 endpoints:

  • /sign : takes as input a string and reply with its ED25519-SHA512 signature
  • /publickey: reply the public key material

There is no authentication neither authorization, except for maybe a special string (not yet implemented) that enables to ensure we do not let, by mistake, an open signing server which could sign any path and create a bigger disaster.
Such things are left to firewalling and reverse proxies measures.

Impact evaluation on signature speed, management of timeouts (exponential backoff), error handling are all out of scope for now, as we are still working on this with @cole-h and potentially more folks who may join us.

Perl bindings will not contain a remote signing version as it's hard to abstract properly the store()->signDetached(...), for whatever is worth, a store could not have a signer, and we could not sign things with it.
The only way is to peek and downcast to a store known to have signers. Such a change would be invasive.

TODO list

  • Rework getPublicKey idea for remote signer to avoid paying a penalty all the time for nothing
  • Validate the remote-signing URL format for easier buildup of API endpoints
  • (Integration?) tests
  • Binary cache store implementation?
  • Communication key (the "secret special string", which is by no means a serious protection, but defense in depth!)
  • Fix Perl bindings (for Hydra)

Priorities

Add 👍 to pull requests you find important.

@github-actions github-actions bot added new-cli Relating to the "nix" command store Issues and pull requests concerning the Nix store labels Sep 30, 2023
@RaitoBezarius RaitoBezarius changed the title libstore: generalize signature scheme libstore: generalize signature scheme and introduce remote signing Sep 30, 2023
src/libstore/globals.hh Outdated Show resolved Hide resolved
Copy link
Member

@lheckemann lheckemann left a comment

Choose a reason for hiding this comment

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

Love to see things happening here!

I wonder if this is fundamentally the right approach, as opposed to sticking to local signatures (with a per-machine key, and the signing done by another machine based on its trust of those per-machine keys). Such an approach would provide somewhat better traceability.

@RaitoBezarius
Copy link
Member Author

I wonder if this is fundamentally the right approach, as opposed to sticking to local signatures (with a per-machine key, and the signing done by another machine based on its trust of those per-machine keys). Such an approach would provide somewhat better traceability.

Wouldn't this require copying the paths to the signing box, then getting those paths back, incurring a potential high network traffic?

@lheckemann
Copy link
Member

No, it could take the fingerprint as signed by the builder and re-sign that.

@RaitoBezarius
Copy link
Member Author

No, it could take the fingerprint as signed by the builder and re-sign that.

Wouldn't this imply making Nix a signing server itself?

@lheckemann
Copy link
Member

lheckemann commented Oct 3, 2023

This functionality wouldn't have to be part of Nix. It could just as well be implemented in a cache server like attic, or as a service that uses Nix to copy and re-sign narinfos from an intermediate flat-file binary cache to the "final" one.

EDIT: and Attic is already 90% of the way there: it stores signatures from the upload, but doesn't currently validate them or deliver them to clients, instead generating a signature using its own per-cache signing key when the narinfo is requested.

@RaitoBezarius
Copy link
Member Author

This functionality wouldn't have to be part of Nix. It could just as well be implemented in a cache server like attic, or as a service that uses Nix to copy and re-sign narinfos from an intermediate flat-file binary cache to the "final" one.

EDIT: and Attic is already 90% of the way there: it stores signatures from the upload, but doesn't currently validate them or deliver them to clients, instead generating a signature using its own per-cache signing key when the narinfo is requested.

Gotcha, but, in practice, for our needs right now, what is the plan you propose with this idea?

I guess this is pushing the problem to an extra piece of software we have to run on the NixOS org machines, right?

@Ericson2314
Copy link
Member

I think this machinery should go in libutil because it is just about signing arbitrary data, and not anything store-specific?

@RaitoBezarius
Copy link
Member Author

I think this machinery should go in libutil because it is just about signing arbitrary data, and not anything store-specific?

I agree with this.

@zimbatm
Copy link
Member

zimbatm commented Oct 21, 2023

So move this in libutil and then this can be merged?

@Ericson2314
Copy link
Member

Yeah if it is moved I support this. Maybe some more rounds of little review after but only little things.

@nixos-discourse
Copy link

This pull request has been mentioned on NixOS Discourse. There might be relevant details there:

https://discourse.nixos.org/t/2023-11-17-nix-team-meeting-minutes-104/35753/1

@Ericson2314 Ericson2314 added the idea approved The given proposal has been discussed and approved by the Nix team. An implementation is welcome. label Nov 20, 2023
@Ericson2314
Copy link
Member

Idea sounds good to people in the nix team meeting. Just need to fix the more minor things addressed in the thread.

@thufschmitt thufschmitt removed the new-cli Relating to the "nix" command label Dec 20, 2023
@github-actions github-actions bot added the new-cli Relating to the "nix" command label Jan 2, 2024
src/libutil/hash.hh Outdated Show resolved Hide resolved
src/libstore/keys.hh Outdated Show resolved Hide resolved
src/libstore/keys.hh Outdated Show resolved Hide resolved
const std::string nixVersion = PACKAGE_VERSION;

Copy link
Member

Choose a reason for hiding this comment

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

Is this used?

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, yes, the symbol needs to be defined in that library? It's used for set_user_agent.

src/libutil/util.cc Outdated Show resolved Hide resolved
Comment on lines 17 to 26
struct Hash;

void initLibUtil();

/**
* This Nix version.
*/
extern const std::string nixVersion;


Copy link
Member

Choose a reason for hiding this comment

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

no changes to this file are needed

Copy link
Member Author

Choose a reason for hiding this comment

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

Why? How do I access nixVersion ?

As it makes more sense to make it available to more consumers.

In this commit, we create a new util: `randomSha1` because `libstore`
depended on sodium to do that in the past. We moved the sodium
initialization and dependency to `libutil`.

Moreover, we keep `getDefaultPublicKeys` inside `libstore` as it relies
on the global variable `settings` to derive this information and is
really only but a consumer of the `libutil`'s signature code.
As we are only reading the string data, this is more idiomatic.
We document the format and the remote signature protocol.
This allows someone oeprating a binary cache store to setup a remote
signing path for remote signing operations.
This avoids useless roundtrips every time we want to inquire about the
public key.
Comment on lines 1 to 47
# Signature

In Nix, signatures are used to trust store paths using asymmetric cryptography,
in this instance: Curve25519-based cryptography.

## Store path signature format

Store path signatures are ED25519 signatures, there are two types of
signatures:

- `ValidPathInfo` will assemble a fingerprint in the form of `1;<store
path>;<NAR hash in base32 including its own type>;<NAR size in bytes>;<NAR
references separated by ,>` and sign this information with the ED25519
private key.
- `Realisation` will assemble a fingerprint in the form of a JSON string: `{
id, outPath, dependentRealisations }` and sign this information with the
ED25519 private key.

# Remote signature protocol

The remote signature protocol is a mechanism to offload the signature of your
store paths to another machine that can possess the secret key material in a
secure way.

In this setup, Nix will contact a remote signing URL that you specified and ask
to sign fingerprints over the wire.

The protocol expects a UNIX domain socket to force you to handle proper
authentication and authorization. `socat` is a great tool to manipulate all
sorts of sockets.

## Semantics of the APIs

- `POST /sign`: expects a fingerprint as input and will return the signature
associated to that fingerprint.
- `POST /sign-store-path`: expects a store path as a parameter and will attempt
to sign that specific store path which is expected to be present on the
signer's machine and return the signature in the response.
- `GET /publickey`: receives the public key as a string in the response.

## A note on the security of that scheme

You are responsible to ensure that `/sign` cannot be abused to sign anything
and everything, for this, a simple setup could involve setting up a TCP service
that requires authentication, e.g. SSH or something on the HTTP level and you
can run a highly privileged daemon on the machine that wants to benefit from
signatures presenting a UNIX domain socket to Nix.
Copy link
Member

Choose a reason for hiding this comment

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

This file should be one sentence per line (or other punctuation / semantic boundary), not fixed line length wrapped.

Copy link
Member Author

Choose a reason for hiding this comment

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

Does that mean that you want to remove the fixed line length wrapping and leave it the existing punctuation or ?

Copy link
Member

Choose a reason for hiding this comment

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

I'll do it. @fricklerhandwerk wrote the non-fixed length lines somewhere in the contributions docs but I forget where. You can find it if you are curious.

Ericson2314 added a commit to obsidiansystems/nix that referenced this pull request Jan 3, 2024
This sets up infrastructure in libutil to allow for signing other than
by a secret key in memory. NixOS#9076 uses this to implement remote signing.

(Split from that PR to allow reviewing in smaller chunks.)

Co-Authored-By: Raito Bezarius <[email protected]>
Ericson2314 added a commit to obsidiansystems/nix that referenced this pull request Jan 3, 2024
This sets up infrastructure in libutil to allow for signing other than
by a secret key in memory. NixOS#9076 uses this to implement remote signing.

(Split from that PR to allow reviewing in smaller chunks.)

Co-Authored-By: Raito Bezarius <[email protected]>
Ericson2314 added a commit that referenced this pull request Jan 3, 2024
…ization

Master included NixOS#9688 prep PR, shrinking the diff. Also the merge
includes a number of slight adjustments from @Ericson2314.
Comment on lines +37 to +39
- `POST /sign-store-path`:

Expects a store path as a parameter and will attempt to sign that specific store path which is expected to be present on the signer's machine and return the signature in the response.
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't exist at the moment, but I wonder whether it should. Yes, I talked about moving things into libutil before, but my experience with hardware parse-display-and-sign devices make me wonder if we should always send over a "nice, JSON" form of what is to be signed, and the other end is responsible for concatenating the actual signature data.

If nothing else, forcing the remote side to munge the bytes is a good defense against lazy blind-singing implementations.

Copy link
Member

Choose a reason for hiding this comment

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

Well, it "exists" in that I implemented it in my service-side of this PR (https://github.com/cole-h/nixos-cache-signing-server), heh.

I do think sending the bits necessary for signing as a well-formed JSON document is a good idea, and will make it harder to misuse.

(The only reason /sign-store-path exists is because I initially implemented it in terms of shelling out to nix, because at the time I couldn't figure out how to do the signing as Nix does it. This has been since rectified, so honestly I wouldn't be opposed to removing it altogether, unless there's a reason to keep it around?)

Copy link
Member Author

Choose a reason for hiding this comment

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

As said privately to @Ericson2314, I don't believe this sign store path API is good for us yet because we are looking at a high frequency signature system, e.g. hydra.nixos.org and there's no distributed filesystems that will make those stores paths available to the signer in time so they can sign them.

For technical reasons, we need a blind signer that is sufficiently resistant to misuse to avoid disasters for random people and ourselves.

Sending a well-formed JSON document looks good to me, but how is it harder to misuse? The only way to make it better is to introduce authnz or auth inside the server and the protocol, this was obviously out of scope because we cannot represent all the usecases in such a simple PR.

Once this lands, my opinion is that we should focus our efforts on understanding how to move the signing to the remote server using PKCS#11 API (which is almost ready with cryptoki and rust-cryptoki thanks to @baloo) and then think from the HSM perspective. There are many missing pieces and knobs around this system of signature: no revocation, no "timestamping", no certification paths, etc. This is understandable why but all those years have passed and those missing features are limiting our operational range in regards to the cache security.

Comment on lines +28 to +29
The protocol expects a UNIX domain socket to force you to handle proper authentication and authorization.
`socat` is a great tool to manipulate all sorts of sockets.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is missing an example of how any of this looks in action. Right now I wouldn't even know where to start in order to use it.

Copy link
Member Author

Choose a reason for hiding this comment

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

I wonder if I should really give an example because I don't want to give the trivial example that will make people run it insecurely, so I can give non-trivial examples to run it securely but with imaginary TCP services.

Copy link
Member

Choose a reason for hiding this comment

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

Yeah I plan on revising the docs a good bit

Copy link
Contributor

Choose a reason for hiding this comment

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

It would be enough to show how to invoke Nix to listen on a socket. I don't see from the docs which interfaces this change even exposes.

@mschwaig
Copy link
Member

mschwaig commented Jan 4, 2024

I wonder if this is fundamentally the right approach, as opposed to sticking to local signatures (with a per-machine key, and the signing done by another machine based on its trust of those per-machine keys). Such an approach would provide somewhat better traceability.

I agree and would like to add to this point. I see it as a big design issue in the existing signing scheme that the signatures do not make any statements about the origin of a path.
An updated signature format could include a cryptographically verifiable statement that the signing host (claims to have) built a given path itself. I think such a signature would serve as a good basis for another host (remote signer) to also produce a signature for the given path.

See #9644 for a more detailed description of that idea.

Comment on lines +12 to +14
1;<store path>;<NAR hash in base32 including its own type>;<NAR size in bytes>;<NAR references separated by ,>
```
and sign this information with the ED25519 private key.
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure what the exact signature scheme is, but if we intend to sign prehashed payload (and use the NAR hash in base32 specified here), ed25519 requires it to be sha512.
(https://docs.rs/ed25519-dalek/latest/ed25519_dalek/trait.DigestSigner.html#impl-DigestSigner%3CD,+Signature%3E-for-Context%3C'_,+'_,+SigningKey%3E)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
idea approved The given proposal has been discussed and approved by the Nix team. An implementation is welcome. new-cli Relating to the "nix" command store Issues and pull requests concerning the Nix store
Projects
Status: 🏁 Review
Development

Successfully merging this pull request may close these issues.

10 participants