-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: master
Are you sure you want to change the base?
libstore: generalize signature scheme and introduce remote signing #9076
Conversation
a71db16
to
78e091b
Compare
There was a problem hiding this 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.
Wouldn't this require copying the paths to the signing box, then getting those paths back, incurring a potential high network traffic? |
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? |
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? |
I think this machinery should go in |
I agree with this. |
So move this in libutil and then this can be merged? |
Yeah if it is moved I support this. Maybe some more rounds of little review after but only little things. |
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 |
Idea sounds good to people in the nix team meeting. Just need to fix the more minor things addressed in the thread. |
78e091b
to
c5e06a4
Compare
src/libutil/util.cc
Outdated
const std::string nixVersion = PACKAGE_VERSION; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this used?
There was a problem hiding this comment.
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.hh
Outdated
struct Hash; | ||
|
||
void initLibUtil(); | ||
|
||
/** | ||
* This Nix version. | ||
*/ | ||
extern const std::string nixVersion; | ||
|
||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
The rest should work out of the box.
This avoids useless roundtrips every time we want to inquire about the public key.
474f63c
to
b912333
Compare
# 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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.
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 <masterancpp@gmail.com>
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 <masterancpp@gmail.com>
…n-prep Signer infrastructure: Prep for #9076
…ization Master included NixOS#9688 prep PR, shrinking the diff. Also the merge includes a number of slight adjustments from @Ericson2314.
286ea7d
to
a58585c
Compare
- `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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?)
There was a problem hiding this comment.
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.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
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. See #9644 for a more detailed description of that idea. |
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. |
There was a problem hiding this comment.
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)
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 anywhereand 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 localPublicKey
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 materialThere 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
getPublicKey
idea for remote signer to avoid paying a penalty all the time for nothingPriorities
Add 👍 to pull requests you find important.