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

add RFC for Flux security signatures #391

Merged
merged 1 commit into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Table of Contents
- [36/Batch Script Directives](spec_36.rst)
- [37/File Archive Format](spec_37.rst)
- [38/Flux Security Key Value Encoding](spec_38.rst)
- [39/Flux Security Signature](spec_39.rst)

Build Instructions
------------------
Expand Down
7 changes: 7 additions & 0 deletions index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,12 @@ of file system objects.
The Flux Security Key Value Encoding is a serialization format
for a series of typed key-value pairs.

:doc:`39/Flux Security Signature <spec_39>`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The Flux Security Signature is a NUL terminated string that represents
content secured with a digital signature.

.. Each file must appear in a toctree
.. toctree::
:hidden:
Expand Down Expand Up @@ -304,3 +310,4 @@ for a series of typed key-value pairs.
spec_36
spec_37
spec_38
spec_39
193 changes: 193 additions & 0 deletions spec_39.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
.. github display
GitHub is NOT the preferred viewer for this file. Please visit
https://flux-framework.rtfd.io/projects/flux-rfc/en/latest/spec_39.html

##########################
39/Flux Security Signature
##########################

The Flux Security Signature is a NUL terminated string that represents
content secured with a digital signature.

- Name: github.com/flux-framework/rfc/spec_39.rst

- Editor: Jim Garlick <[email protected]>

- State: raw

********
Language
********

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to
be interpreted as described in `RFC 2119 <https://tools.ietf.org/html/rfc2119>`__.

*****************
Related Standards
*****************

- :doc:`14/Canonical Job Specification <spec_14>`

- :doc:`15/Independent Minister of Privilege for Flux: The Security IMP <spec_15>`

- :doc:`38/Flux Security Key Value Encoding <spec_38>`

**********
Background
**********

The Flux Security IMP is the privileged component of Flux responsible for
launching tasks in a multi-user Flux installation. Each job request is
signed by the submitting user and verified by the IMP as a precondition for
performing the user transition and starting tasks. Signing deters tampering
when the job passes through the unprivileged Flux instance. This is the
primary use case for the Flux Security Signature.

A job request is expressed as *jobspec* (RFC 14). Importantly, jobspec
contains the proposed command line to run as well as the current working
directory and environment. Signing the jobspec ensures that these parameters
are passed to the IMP exactly as specified by the user.

RFC 15 refers to the signed jobspec as *J*. The content of *J* is thus fully
specified by this document and RFC 14.

*****
Goals
*****

- The signature mechanism SHOULD be configurable so that a site's existing
security infrastructure can be used, if applicable.

- The signature payload MUST accommodate the maximum size of a serialized
RFC 14 jobspec.

- The signature MUST be a UTF-8 string that does not contain NUL characters.

- The signature SHOULD support a configurable time-to-live.

**************
Implementation
**************

The Flux Security Signature is structured like the JSON Web Signature
Compact Serialization of `RFC 7515 <https://tools.ietf.org/html/rfc7515>`__.
It SHALL contain three components concatenated with period (``.``) delimiters:

HEADER.PAYLOAD.SIGNATURE

- The *header* SHALL be formatted using Flux Security Key Value Encoding
(RFC 38), then base64 encoded. The REQUIRED keys are listed below.
The header is not encrypted.

- The *payload* is an arbitrary sequence of zero or more bytes that
SHALL be base64 encoded. The payload is not encrypted.
Copy link

Choose a reason for hiding this comment

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

Do you want to specify a particular variant of base64?

Copy link
Member Author

Choose a reason for hiding this comment

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

Great point. Looks like we're using rfc4648 "standard base64". I'll call that out.

In flux-security, we use libsodium SODIUM_BASE64_VARIANT_ORIGINAL

In flux-core we use ccan base64_maps_rfc4648.


- The *signature* SHALL be a mechanism-dependent UTF-8 string that represents
a signature over the other two components including the delimiter.
The signature string SHALL NOT contain the NUL or period (``.``) characters.

The header and payload SHALL be encoded in the
`RFC 4648 <https://tools.ietf.org/html/rfc4648>`__ standard variant of base64.

The header SHALL contain the following keys:

version (integer)
Copy link

Choose a reason for hiding this comment

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

How are integers encoded? UTF-8 string? binary values of a certain length? MSBF? etc.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is defined in RFC 38.

The Flux Security Signature version (currently must be set to 1).

mechanism (string)
Copy link

Choose a reason for hiding this comment

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

How is this string encoded? NUL terminated?

Or are integer and string encodings defined in another RFC?

The mechanism used to create signature component.

userid (integer)
The claimed userid.

Mechanism-specific keys MAY also be included in the header.

**********
Mechanisms
**********

Signing mechanisms include:

MUNGE
=====

The *munge* mechanism uses the `MUNGE <https://github.com/dun/munge/wiki>`__,
authentication service, which is simple to set up, and is often already
present on HPC clusters.

The header *mechanism* key SHALL be set to the string ``munge``. No
mechanism-specific header keys are required.

Signing SHALL consist of the following steps:

#. Compute the 32 byte SHA-256 hash digest over HEADER.PAYLOAD

#. Construct a 33 byte MUNGE payload by concatenating a single byte *hash type*
field of ``0x01`` (indicating SHA-256) and the hash digest computed above.

#. Encode the payload with ``munge_encode(3)`` to obtain the MUNGE credential
which becomes the signature component.

.. note::
MUNGE credentials conform to the signature component requirement of being a
UTF-8 string without embedded NUL characters or periods. See also: MUNGE
`Credential Format <https://github.com/dun/munge/wiki/Credential-Format>`__.

Verification SHALL consist of the following steps:

#. Decode the MUNGE credential with ``munge_decode(3)`` to obtain the MUNGE
payload and the MUNGE-authenticated userid. Return values of EMUNGE_SUCCESS,
EMUNGE_CRED_REPLAYED, or EMUNGE_CRED_EXPIRED SHALL be treated as success.

#. Re-compute the 32 byte SHA-256 hash digest over HEADER.PAYLOAD

#. Check that the MUNGE payload consists of 33 bytes: a single byte *hash type*
field of ``0x01`` followed by the same 32 byte SHA-256 hash digest computed
above.

#. Check that the MUNGE-authenticated userid matches the header *userid* field.

#. Call ``munge_ctx_get(3)`` on MUNGE_OPT_ENCODE_TIME to obtain the wall clock
time that the credential was encoded.

#. Check that the time elapsed since the credential was encoded does not
exceed the configured site time-to-live.

.. note::
MUNGE imposes a short time-to-live on its credentials, but since
Flux job requests may be pending for *days*, expired credential errors
from MUNGE are ignored and a site configured time-to-live is imposed instead.
Similarly, MUNGE detects replay attacks by only allowing a credential to be
decoded once per host, but since Flux job requests may need to be verified
by multiple parties on the same node, replay errors from MUNGE are ignored.

NONE
====

The *none* mechanism is useful for testing and for situations where the
signature format is needed for consistency but the veracity of the signature
is not important, such as in a single-user Flux instance.

The header *mechanism* key SHALL be set to the string ``none``. No
mechanism-specific keys are required. The *none* mechanism does not support
a time-to-live.

Signing SHALL consist of the following steps:

#. Set the signature component to the string ``none``.

Copy link
Contributor

Choose a reason for hiding this comment

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

For the curious, include how header and payload are constructed for sign-type=none?

Copy link
Member Author

Choose a reason for hiding this comment

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

The header and payload are the same regardless of mechanism and can be read or written in a mechanism independent manner. Maybe I'd better call that out explicitly!

Well that statement is almost true - the header can optionally contain mechanism specific values, although for none and munge it does not.

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh duh, sorry you do explicitly state that above but it may be helpful to re-state it in the 'none' section. Sorry!

Verification SHALL consist of the following steps:

#. Check that the signature component is the string ``none``.

#. Check that the real user id matches the header userid.

Copy link
Contributor

Choose a reason for hiding this comment

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

Include how payload is extracted?

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, you do state that the payload is always base64 encoded, my bad!

.. note::
Requiring that the header userid matches the real user id deters use of
the *none* mechanism in inappropriate situations, e.g. as a precondition
to performing a user transition in the IMP.

.. note::
As a reminder, the payload and header components are simply base64 encoded
and may be read or written in a mechanism independent manner.