Subject: [Suit] Comments on manifest format

From: Rønningstad, Øyvind <[email-masked]>
Date: Tue, 15 May 2018 15:49:39 +0000

Below are some comments and questions regarding the manifest format in draft-moran-suit-architecture-03 [architecture] and draft-moran-suit-manifest-01 [manifest]. They are the result of a thorough review and discussion which also yielded ideas for alternative manifest formats, which we hope will be helpful when deciding the standard for the format. At the end of the email are three proposals ([alt1], [alt2], [alt3]), with progressively bigger changes. We have tried to keep intact the primary purpose of the elements that have changed, unless there was a reason to change it, in which we have tried to describe the change and the motivation behind it.

  1.  The extensions field of the manifest should have some explanation.
  2.  Also, it would be useful to have a short explanation of how storageIdentifier can be used (e.g. why is it a bstr?). Will it incorporate the destination address of the update? Perhaps storageIdentifier could also be a URI [alt3]. [US1]
  3.  There’s a discrepancy between how the payload resource is referenced and how dependencies are referenced, is there a reason for this? If not, see [alt1] for a suggestion for a more uniform way to represent linked resources. This has a few benefits:
     *   All URIs are in one place so the transport code can retrieve all resources with minimal understanding of the Manifest format. [US2]
     *   The restrictions in [manifest] section 3.7 can be simplified.
  4.  For dependencies: Will the linked manifests be subject to the same validation as the main manifest, or will e.g. the signature be ignored?
  5.  The list in [architecture] section A.5.5 lists compression along with other formats. However, compression can be applied to any payload format (as can encryption), e.g. you can have compressed elf or compressed delta, so maybe there should be a field compressionAlgorithm, together with a field size, see [alt1]. [US3]
  6.  Consider not allowing embedded payloads in manifests, see [alt2]. They make some things easier, but also make some things more complicated:
     *   The parser must be able to handle both cases.
     *   The unparsed manifest will likely not fit in RAM.
     *   It delays the time at which the manifest can be validated until after the whole payload has been received.
Alternatively, the payload is always a reference, and we define a specific URI that means “concatenated with this” to be used e.g. in the following cases (here, the URI for all payloads and dependencies is “concatenated with this”):

  1.  The device receives a single blob that contains the manifest concatenated with the payload.
  2.  The device receives 3 blobs:

     *   The manifest
     *   The payload
     *   Dependency manifest concatenated with its payload

References

Documents:
[architecture]: draft-moran-suit-architecture-03 - https://datatracker.ietf.org/doc/draft-moran-suit-architecture/03
[manifest]: draft-moran-suit-manifest-01 - https://datatracker.ietf.org/doc/draft-moran-suit-manifest/01
[tls1.2]: The Transport Layer Security (TLS) Protocol Version 1.2 - https://tools.ietf.org/html/rfc5246
[tlscompression]: Transport Layer Security Protocol Compression Methods - https://tools.ietf.org/html/rfc3749

User Stories:
[US1]: I want to update my bootloader, or as big a part of it as possible, to be able to fix bugs or add features.
[US2]: I want to split my bootloader into 3 parts: Transport (can be in app), validation and activation.
[US3]: I want to compress my firmware to reduce the transfer time, and to reduce the space needed for the intermediate copy on my device.
[US4]: To reduce the risk of bugs, I want a manifest format that can be expressed in a schema, so the parser can be auto-generated.

Proposed Manifest Formats:
[alt1]: Alternative Manifest format with all linked resources in one place and on one format. Adapted from [manifest] Section 4. digestAlgorithm in ResourceReference refers to the digest in the same ResourceReference. This proposal also adds compressionInfo which, if it is present in payloadInfo, indicates that the payload (as plaintext) has been compressed. CompressionInfo.algorithm could adhere to [tlscompression].

   condition = [
       type : int,
       parameters : bstr
   ]
   directive = condition

   ResourceReference = [
       uris: [*[
           rank: int,
           uri: tstr
       ]],
       size: uint,
       digestAlgorithm = [
           type : int,
           ? parameters: bstr
       ] / nil,
       digest: bstr
   ]

   PayloadInfo = [
       format = [
           type: int,
           ? parameters : bstr
       ],
       size: uint,
       storageIdentifier: bstr,
       digestAlgorithm = [
           type : int,
           ? parameters: bstr
       ] / nil,
       digests = {* int => bstr} / nil,
       compressionInfo = [
           type : int,
           size : uint
       ] / nil,
       payload: COSE_Encrypt / bstr / nil
   ]

   Manifest = [
       manifestVersion : uint,
       aliases: [ * ResourceReference ] / nil,
       dependencies: [ * ResourceReference ] / nil,
       payloadUri: ResourceReference / nil,
       text : {* int => tstr } / nil,
       nonce : bstr,
       timestamp : uint,
       conditions: [ * condition ],
       directives: [ * directive ] / nil,
       extensions: { * int => bstr } / nil,
       payloadInfo: ? payloadInfo,
   ]

[alt2]: Alternative Manifest format (more radical changes). Changes from [alt1]:

  1.  Consolidate several types to a TypeStream type.
  2.  Separate payload and encryption metadata. The COSE_Encrypt structure in encryptionInfo will always have a nil payload. If it is present, the payload is encrypted. This removes the possibility for embedded payloads.
  3.  DigestAlgorithm is a uint without parameters because that should be sufficient (see e.g. HashAlgorithm in [tls1.2]).
  4.  Represent Conditions and Directives with value choices (enums). [US4]

   TypeStream = [
       type: int,
       ? parameters: bstr
   ]

   Condition = [
       type: vendorId: 1 / classId: 2 / deviceId: 3 / bestBefore: 4 / nint,
       parameters: bstr,
   ]

   Directive = [
       type: applyImmediately: 1 / applyAfter: 2 / nint,
       parameters: bstr,
   ]

   ResourceInfo = [
       uris: [*[
           rank: int,
           uri: tstr
       ]],
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
   ]

   CompressionInfo = [
       compressionAlgorithm: uint,
       size: uint,
   ]

   PayloadInfo = [
       format: TypeStream,
       storageIdentifier: bstr / nil,
       digestAlgorithm: uint / nil,
       digests: { * int => bstr } / nil,
       compressionInfo: CompressionInfo / nil,
       encryptionInfo: COSE_Encrypt / nil,
   ]

   Manifest = [
       manifestVersion: uint,
       nonce: bstr,
       timestamp: uint,
       payload: ResourceInfo,
       dependencies: [ * ResourceInfo ] / nil,
       text: {* int => tstr } / nil,
       aliases: [ * tstr ] / nil,
       conditions: [ * Condition ] / nil,
       directives: [ * Directive ] / nil,
       extensions: { * int => bstr } / nil,
       payloadInfo: PayloadInfo,
   ]

[alt3]: Alternative Manifest format (even more radical changes). Changes from [alt2]:

  1.  Move storageIdentifier to top level since it is an important part of the initial validation of the manifest, and serves as a premise for the entire update. Rename it to updateSubject.
  2.  Define StorageIdentifier as a URI.
  3.  Change Uri type from list of lists to map, it seems a bit cleaner.
  4.  Use CDDL Generics to define types for all predefined conditions, as well as a specialized type for custom directives. [US4]
  5.  Add the isRoot flag. If this is true, the manifest can act as an “original” or “root” manifest. If false, it can only be applied as a dependency. If false, the manifest doesn’t have to be signed (although it can). The root flag provides the following benefits:
     *   It allows implementation of the ApplyImmediately directive without directives
     *   It gives protection from a type of attack where a manifest meant as a dependency is applied as root. Such an attack could introduce API-breaking changes without updating the application that uses of the API.
     *   It allows making lightweight non-root manifests without signatures, since their integrity is guaranteed by the root manifest’s signature (via dependency digests).
  6.  Make applyAfter a condition instead of directive, essentially moving the deferred update out of scope of this spec. The update will now just fail if the condition is not met.
  7.  Since all directives are moved elsewhere, remove directives. Depending on the final role of extensions, custom directives can be added as extensions instead.
  8.  Introduce ResourceState (generalized from ResourceInfo) as a standard way of specifying something that should be done, and the size and digest of the resource afterwards.
  9.  The precursor image digest is implemented as a condition (ImageStateCondition). The condition takes a StorageIdentifier as parameter so the check can be applied to parts not directly touched by the update (as specified in updateSubject).

   CompressionAlgorithm = uint
   PayloadFormat        = uint
   Uri                  = tstr
   UriList              = { + int => Uri }
   TimeStamp            = uint .size 8
   Uuid                 = bstr .size 16
   StorageIdentifier    = Uri

   TypeStream<input1,input2> = [
       type: input1,
       ? parameters: input2
   ]

   ResourceState<input1> =
       parameters: input1
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
   ]

   VendorIdCondition   = TypeStream<1, Uuid>
   ClassIdCondition    = TypeStream<2, Uuid>
   DeviceIdCondition   = TypeStream<3, Uuid>
   BestBeforeCondition = TypeStream<4, TimeStamp>
   ApplyAfterCondition = TypeStream<5, TimeStamp>
   ImageStateCondition = TypeStream<6, ResourceState<StorageIdentifier>>
   CustomCondition     = TypeStream<nint, bstr>
   Condition = VendorIdCondition / ClassIdCondition / DeviceIdCondition / BestBeforeCondition / ApplyAfterCondition / ImageStateCondition / CustomCondition

   ApplyInfo = [
       encryption:  ResourceState<COSE_Encrypt> / nil,
       compression: ResourceState<CompressionAlgorithm> / nil,
       format:      ResourceState<PayloadFormat> / nil
   ]

   Manifest = [
       manifestVersion: uint,
       nonce: bstr,
       timestamp: uint,
       isRoot: bool,
       updateSubject: StorageIdentifier,
       dependencies: [ + ResourceState<UriList> ] / nil,
       payload: ResourceState<UriList>,
       conditions: [ + Condition ] / nil,
       payloadApplyInfo: ApplyInfo,
       text: { + int => tstr } / nil,
       manifestAliases: Uris / nil,
       extensions: { + int => bstr } / nil
   ]

Best Regards,

Øyvind Rønningstad
Frank Audun Kvamtrø
Krzysztof Chruściński
Andrzej Puzdrowski

Nordic Semiconductor ASA

---

From: Brendan Moran <[email-masked]>
Date: Tue, 22 May 2018 18:21:05 +0000

Øyvind Rønningstad, Frank Audun Kvamtrø, Krzysztof Chruściński, Andrzej Puzdrowski,

Thank you very much for the in depth analysis and comments!

________________________________
From: Suit <[email-masked]> on behalf of Rønningstad, Øyvind <[email-masked]>
Sent: Tuesday, May 15, 2018 4:49 PM
To: [email-masked]
Subject: [Suit] Comments on manifest format

     *   The manifest
     *   The payload

References

Documents:

User Stories:

   condition = [

       type : int,

       parameters : bstr

   ]

   directive = condition

   ResourceReference = [

       uris: [*[

           rank: int,

           uri: tstr

       ]],

       size: uint,

       digestAlgorithm = [

           type : int,

           ? parameters: bstr

       ] / nil,

       digest: bstr

   ]

   PayloadInfo = [

       format = [

           type: int,

           ? parameters : bstr

       ],

       size: uint,

       storageIdentifier: bstr,

       digestAlgorithm = [

           type : int,

           ? parameters: bstr

       ] / nil,

       compressionInfo = [

           type : int,

           size : uint

       ] / nil,

   ]

   Manifest = [

       manifestVersion : uint,

       nonce : bstr,

       timestamp : uint,

   ]

   TypeStream = [

       type: int,

       ? parameters: bstr

   ]

   Condition = [

       parameters: bstr,

   ]

   Directive = [

       parameters: bstr,

   ]

   ResourceInfo = [

       uris: [*[

           rank: int,

           uri: tstr

       ]],

       size: uint,

       digestAlgorithm: uint,

       digest: bstr

   ]

   CompressionInfo = [

       size: uint,

   ]

   PayloadInfo = [

       format: TypeStream,

   ]

   Manifest = [

       manifestVersion: uint,

       nonce: bstr,

       timestamp: uint,

       payload: ResourceInfo,

   ]

   PayloadFormat        = uint

   Uri                  = tstr

   TimeStamp            = uint .size 8

   Uuid                 = bstr .size 16

   StorageIdentifier    = Uri

       type: input1,

       ? parameters: input2

   ]

   ResourceState<input1> =

       parameters: input1

       size: uint,

       digestAlgorithm: uint,

       digest: bstr

   ]

   ApplyInfo = [

   ]

   Manifest = [

       manifestVersion: uint,

       nonce: bstr,

       timestamp: uint,

       isRoot: bool,

   ]

Best Regards,

Øyvind Rønningstad

Frank Audun Kvamtrø

Krzysztof Chruściński

Andrzej Puzdrowski

Nordic Semiconductor ASA

IMPORTANT NOTICE: The contents of this email and any attachments are confidential and may also be privileged. If you are not the intended recipient, please notify the sender immediately and do not disclose the contents to any other person, use it for any purpose, or store or copy the information in any medium. Thank you.

---

From: Brendan Moran <[email-masked]>
Date: Wed, 23 May 2018 11:05:16 +0000

Øyvind Rønningstad, Frank Audun Kvamtrø, Krzysztof Chruściński, Andrzej Puzdrowski,
Thanks again for your comments. I think that we can make some improvements to the proposed serialisation format.

  1.
Extensions are intended to enable new features to be added to the manifest without substantial changes to the parser or schema, much like X.509 extensions. At IETF101, the point was raised that we should use a CBOR map rather than a CBOR array, which makes the question of extensions moot. I’m not certain about this; I believe it requires further analysis. The issue I see is that extensions can be marked as optional or mandatory, since they follow a standard schema, whereas map entries cannot, since they can be any value.

Given how the manifest contains very little extraneous data, it might be fair to say that all fields, including extensions, are mandatory. Another option might be to declare a schema reference in the manifest, which specifies which fields are mandatory.

  2.
First some terminology. There are two possible architectures of device:
     *   Uniform Memory Architecture (UMA): all memory is within the same address space (For example, a linux system or a microcontroller)
     *   Non-Uniform Memory Architecture (NUMA): memory resides within multiple address spaces (For example a microcontroller system with a UART WiFi adaptor)

The storage identifier is intended to inform the target device where it should install the payload.

In the case of a UMA system, this might be:
     *   a start address
     *   an enum
     *   an identifier
     *   a file path
     *   a URI

In the case of a NUMA system, this might be:
     *   an enum
     *   an identifier
     *   a file path
     *   a URI
     *   A combination of any of the above, including a specifier of which address space to use, and a specifier of where in the address space to install it.

This last case would allow an update to, for example, designate where in a UART-connected WiFi adaptor to write an update.

It seems that this identifier is most likely to be entirely application dependent. Not wanting to specify it too strictly without understanding all the use-cases, we used a bstr since it was the most generic option without increasing the parsing depth. We could specify “any” instead of “bstr” but this would subject the parser to a more deeply nested structure.

Should more of this description be added into the information model?

As for the basis of this field, storageIdentifier (Storage Location in the Information Model) satisfies part of Security Requirement MFSR4: Signed Payload Descriptor, and mitigates Threat MFT5: The target device installs the payload to the wrong location.

  3.
Maybe we can improve the whole manifest by replacing aliases, payload, and dependencies.

I think that the ResourceReference structure you describe is a good improvement. I might suggest changing it so that there is an additional type field, which specifies whether it is a data file, a manifest (dependencies), or an alias. This would be a break from the current mechanism which separated out data files from manifests specifically to enforce that there is only one data file. I’m not sure how multiple data files would work just yet, since there should still be only a single storageIdentifier, but it’s possible that we could use this along with another improvement I have in mind. See processing steps below.

  4.  I think this will depend on the claims made by each manifest. If sufficient claims are asserted to authorise the update before all the manifests are processed, then no other signatures need to be verified, since there are adequate claims and there’s a chain of trust to all manifests based in the linking of one manifest to another by hash.
  5.
I agree, this is not complete. The original intent was that there would be an enum for each option, so that any combination can be referenced by a single number, which is much smaller in data size than defining each of the steps, however I think that compression may be sufficiently complex that it warrants its own section. Because of this, I wonder if it might be better to define “processing steps” these would occur in order and they would instruct the device on how to process the update. For example:

     *   Decrypt (with parameters)
     *   Decompress (with parameters)
     *   De-delta

A similar approach could be used with elf:

     *   Decrypt (with parameters)
     *   Decompress (with parameters)
     *   Apply elf

A relocatable elf might require an extra step to apply the relocation.

It might be that just three steps is sufficient to handle all possible installation configurations, but I’m not confident that this is the case. Processing steps would be much more flexible.

  6.  Thinking about it a bit more, I agree. Anything you can do with an embedded payload you can also do with a concatenated payload, and the parser logic is simpler.

[US1]: I think it’s important to draw out what SUIT means by bootloader. The SUIT bootloader is precisely the smallest part of the system that must be non-updatable in order to guarantee reliability in the face of power failures, etc. There is nothing to say that the system bootloader can’t be updatable, just that as far as SUIT is concerned, that is an application. This is where multi-stage boot comes in.
[US2,3,4]: I fully agree with these user stories. The only reason they aren’t in the current threat model is that they weren’t necessary to justify any part of the information model.

I’m working on an update to the proposed serialisation. I’ll see what I can bring into the CDDL from the alternates that you’ve suggested. I think there are a lot of good ideas in there.

Best Regards,
Brendan

On 15 May 2018, at 16:49, Rønningstad, Øyvind <[email-masked]<mailto:[email-masked]>> wrote:

Below are some comments and questions regarding the manifest format in draft-moran-suit-architecture-03[architecture] and draft-moran-suit-manifest-01 [manifest]. They are the result of a thorough review and discussion which also yielded ideas for alternative manifest formats, which we hope will be helpful when deciding the standard for the format. At the end of the email are three proposals ([alt1], [alt2], [alt3]), with progressively bigger changes. We have tried to keep intact the primary purpose of the elements that have changed, unless there was a reason to change it, in which we have tried to describe the change and the motivation behind it.

  2.  Also, it would be useful to have a short explanation of how storageIdentifier can be used (e.g. why is it a bstr?).Will it incorporate the destination address of the update? Perhaps storageIdentifier could also be a URI [alt3]. [US1]

     *   The manifest
     *   The payload

References

Documents:

User Stories:

   condition = [
       type : int,
       parameters : bstr
   ]
   directive = condition

   ResourceReference = [
       uris: [*[
           rank: int,
           uri: tstr
       ]],
       size: uint,
       digestAlgorithm = [
           type : int,
           ? parameters: bstr
       ] / nil,
       digest: bstr
   ]

   PayloadInfo = [
       format = [
           type: int,
           ? parameters : bstr
       ],
       size: uint,
       storageIdentifier: bstr,
       digestAlgorithm = [
           type : int,
           ? parameters: bstr
       ] / nil,

   Manifest = [
       manifestVersion : uint,

  1.  Consolidate several types to a TypeStream type.
  2.  Separate payload and encryption metadata. The COSE_Encrypt structure in encryptionInfo will always have a nil payload. If it is present, the payload is encrypted. This removes the possibility for embedded payloads.
  3.  DigestAlgorithm is a uint without parameters because that should be sufficient (see e.g. HashAlgorithm in[tls1.2]).
  4.  Represent Conditions and Directives with value choices (enums). [US4]

   TypeStream = [
       type: int,
       ? parameters: bstr
   ]

   Condition = [

   Directive = [

   ResourceInfo = [
       uris: [*[
           rank: int,
           uri: tstr
       ]],
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
   ]

   CompressionInfo = [

   PayloadInfo = [
       format: TypeStream,

   Manifest = [
       manifestVersion: uint,
       nonce: bstr,
       timestamp: uint,
       payload: ResourceInfo,

   ResourceState<input1> =
       parameters: input1
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
   ]

   ApplyInfo = [

   Manifest = [
       manifestVersion: uint,
       nonce: bstr,
       timestamp: uint,
       isRoot: bool,

Best Regards,

Øyvind Rønningstad
Frank Audun Kvamtrø
Krzysztof Chruściński
Andrzej Puzdrowski

Nordic Semiconductor ASA
_______________________________________________
Suit mailing list
[email-masked]<mailto:[email-masked]>
https://www.ietf.org/mailman/listinfo/suit

---

From: David Brown <[email-masked]>
Date: Wed, 23 May 2018 11:54:03 -0600

On Wed, May 23, 2018 at 11:05:16AM +0000, Brendan Moran wrote:

>     1. Decrypt (with parameters)
>     2. Decompress (with parameters)
>     3. De-delta

Often, delta-encoding is really just a special case of compression.
For example, it could be considered a compression with the old image
already present in the compression dictionary.

It will depend on the delta algorithm, but if we're going to enumerate
them, we will need to consider the case where those two steps are the
same.

David

---

From: Rønningstad, Øyvind <[email-masked]>
Date: Mon, 28 May 2018 14:53:05 +0000

Hi Brendan

Bootloader
In the previous comments, the term “bootloader” was meant as “implementation of SUIT”. I agree that might be dubious, so I’ll instead informally call it “agent”.

Extensions
Thanks for the clarification, that makes sense. As a general comment, I think such background information is useful as a part of the spec even if it’s mostly educational.
As for optional vs mandatory, I think it’s useful if a given parser can restrict the format (e.g. via a customized schema) of the manifest beyond what the spec mandates, for example by disallowing extensions or dependencies. That means agents can be simplified by removing the logic required to handle those, and it reduces the chance that an update is misinterpreted by the agent. Marking each extension with whether it is mandatory or not seems useful in that regard, because makes it explicit how an agent should handle an extension it doesn’t understand.

Storage Identifier
Your explanation of StorageIdentifiers captures well what we also came to realize while thinking about it and, again, I think putting some info about UMA or NUMA in the spec would be helpful to guide people into the right frame of mind and to broadly understand what StorageIdentifiers are about.

We’ve been thinking quite a bit about this now, and we would like to propose another addition to the manifest format, see [componentIdentifier] below.

Dependencies
I think it is worth keeping the limitation of only one payload. Additional payloads can be added as dependencies with their own manifest since that is an existing, valid way to achieve the same thing. Our proposal of isRoot and optional signatures for non-root manifests was intended to help this by reducing the overhead of dependencies. The same goes for mandating that a digest is present for all ResourceReferences, so only one signature must be checked.

As for the question about what validation should happen to dependency manifests, the question extends to every field. I.e. which steps of the validation of a manifest should be different if it is a dependency? I think this should be specified explicitly in the spec. E.g. maybe some conditions don’t need be checked in dependencies. It’s quite possible the conclusion will be that all manifests must be validated completely (except signature).

Processing steps
We also considered this, it is much more flexible. In my opinion we don’t need it with the current information model since we only have 3 steps - decrypt, decompress, apply - which must happen in that order.

Looking forward to your updated serialization format.

Best regards
Øyvind and Frank

Appendix
[componentIdentifier]:
Originally, we thought this would be part of the StorageIdentifier, but we moved it outside. Our proposal is to add a ComponentIdentifier to each dependency, so dependencies will have the compound type DependencyInfo as follows:

    ComponentIdentifier  = bstr / nil

    DependencyInfo = [
        component: ComponentIdentifier,
        manifest:  ResourceState<UriList>
        aliases:   [ + ResourceState<UriList> ]
    ]

(Note that we've also added aliases as a member of DependencyInfo under the assumption that aliases always pertain to dependencies’ URI lists.)

We distinguish between a top-level agent and sub-level agent under the assumption that (particularly in NUMA systems) it will sometimes make sense to have multiple agents each reigning over their own address space. These different agents might not trust fully each other, i.e. they will each fully validate manifests and signatures. The top-level agent will be the one to parse the root manifest, but since it doesn’t have direct control over every sub-system, it will hand dependencies over to sub-level agents who will perform the update of their respective sub-systems (which might involve handing over to sub-sub-level agents etc.).

The reason for keeping the componentIdentifier out of the storageIdentifier is so that:

  *   The top-level agent does not necessarily need to parse the dependency manifests it passes to sub-level agents.
  *   The sub-level agents don’t need to know the componentIdentifier namespace.

If component is nil, the dependency will be resolved by the current agent.

An example of the difference between a top-level agent and a sub-level agent would be in a multi core architecture, where the external core needs to be updated in synchronization with the main core if they have API compatibility requirements, so the top-level agent will tell the sub-level agent(s) when to perform the update.

Actions taken by a top-level agent:

  1.  Parse the manifest and check availability of the sub-level agent(s).
  2.  Instruct the sub-level agent(s) to apply dependency manifests.
  3.  (Optional) Instruct the sub-level agent(s) to pre-validate dependency manifests.
  4.  (Optional) Provide a list of aliases to the sub-level agent(s).
  5.  (Optional) Retrieve manifests and payloads on the sub-level agents’ behalf if they don’t have network capabilities.

[alt4]:
Here is a full, updated manifest description based on [alt3] in the previous e-mail:

    CompressionAlgorithm = uint
    PayloadFormat        = uint
    Uri                  = tstr
    UriList              = { + int => Uri }
    TimeStamp            = uint .size 8
    Uuid                 = bstr .size 16
    StorageIdentifier    = bstr / nil
    ComponentIdentifier  = bstr / nil

    ResourceState<input1> =
       parameters: input1,
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
    ]

    ApplyInfo = [

    ResourceReference = ResourceState<UriList>

    DependencyInfo = [
        component: ComponentIdentifier,
        manifest:  ResourceReference,
        aliases:   [ + ResourceReference ]
    ]

    Manifest = [
        manifestVersion:  uint,
        nonce:            bstr,
        timestamp:        uint,
        isRoot:           bool,
        updateSubject:    StorageIdentifier,
        dependencies:     [ + DependencyInfo ] / nil,
        payload:          ResourceReference,
        conditions:       [ + Condition ] / nil,
        payloadApplyInfo: ApplyInfo,
        text:             { + int => tstr } / nil,
        extensions:       { + int => bstr } / nil
    ]

From: Brendan Moran [mailto:[email-masked]]
Sent: 23. mai 2018 13:05
To: Rønningstad, Øyvind <[email-masked]>
Cc: [email-masked]
Subject: Re: [Suit] Comments on manifest format

  1.  Extensions are intended to enable new features to be added to the manifest without substantial changes to the parser or schema, much like X.509 extensions. At IETF101, the point was raised that we should use a CBOR map rather than a CBOR array, which makes the question of extensions moot. I’m not certain about this; I believe it requires further analysis. The issue I see is that extensions can be marked as optional or mandatory, since they follow a standard schema, whereas map entries cannot, since they can be any value.

  1.  First some terminology. There are two possible architectures of device:

     *   a start address
     *   an enum
     *   an identifier
     *   a file path
     *   a URI

     *   an enum
     *   an identifier
     *   a file path
     *   a URI

  1.  Maybe we can improve the whole manifest by replacing aliases, payload, and dependencies.

  1.  I think this will depend on the claims made by each manifest. If sufficient claims are asserted to authorise the update before all the manifests are processed, then no other signatures need to be verified, since there are adequate claims and there’s a chain of trust to all manifests based in the linking of one manifest to another by hash.

  1.  I agree, this is not complete. The original intent was that there would be an enum for each option, so that any combination can be referenced by a single number, which is much smaller in data size than defining each of the steps, however I think that compression may be sufficiently complex that it warrants its own section. Because of this, I wonder if it might be better to define “processing steps” these would occur in order and they would instruct the device on how to process the update. For example:

  1.  Thinking about it a bit more, I agree. Anything you can do with an embedded payload you can also do with a concatenated payload, and the parser logic is simpler.

Best Regards,
Brendan

  1.  For dependencies: Will the linked manifests be subject to the same validation as the main manifest, or will e.g. the signature be ignored?
  2.  The list in [architecture] section A.5.5 lists compression along with other formats. However, compression can be applied to any payload format (as can encryption), e.g. you can have compressed elf or compressed delta, so maybe there should be a field compressionAlgorithm, together with a field size, see [alt1]. [US3]
  3.  Consider not allowing embedded payloads in manifests, see [alt2]. They make some things easier, but also make some things more complicated:

     *   The parser must be able to handle both cases.
     *   The unparsed manifest will likely not fit in RAM.
     *   It delays the time at which the manifest can be validated until after the whole payload has been received.
Alternatively, the payload is always a reference, and we define a specific URI that means “concatenated with this” to be used e.g. in the following cases (here, the URI for all payloads and dependencies is “concatenated with this”):
a.       The device receives a single blob that contains the manifest concatenated with the payload.
b.       The device receives 3 blobs:
a.       The manifest
b.       The payload
c.       Dependency manifest concatenated with its payload

References

Documents:

User Stories:

   condition = [
       type : int,
       parameters : bstr
   ]
   directive = condition

   ResourceReference = [
       uris: [*[
           rank: int,
           uri: tstr
       ]],
       size: uint,
       digestAlgorithm = [
           type : int,
           ? parameters: bstr
       ] / nil,
       digest: bstr
   ]

   PayloadInfo = [
       format = [
           type: int,
           ? parameters : bstr
       ],
       size: uint,
       storageIdentifier: bstr,
       digestAlgorithm = [
           type : int,
           ? parameters: bstr
       ] / nil,

   Manifest = [
       manifestVersion : uint,

   TypeStream = [
       type: int,
       ? parameters: bstr
   ]

   Condition = [

   Directive = [

   ResourceInfo = [
       uris: [*[
           rank: int,
           uri: tstr
       ]],
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
   ]

   CompressionInfo = [

   PayloadInfo = [
       format: TypeStream,

   Manifest = [
       manifestVersion: uint,
       nonce: bstr,
       timestamp: uint,
       payload: ResourceInfo,

  1.  Make applyAfter a condition instead of directive, essentially moving the deferred update out of scope of this spec. The update will now just fail if the condition is not met.
  2.  Since all directives are moved elsewhere, remove directives. Depending on the final role of extensions, custom directives can be added as extensions instead.
  3.  Introduce ResourceState (generalized from ResourceInfo) as a standard way of specifying something that should be done, and the size and digest of the resource afterwards.
  4.  The precursor image digest is implemented as a condition (ImageStateCondition). The condition takes a StorageIdentifier as parameter so the check can be applied to parts not directly touched by the update (as specified in updateSubject).

   ResourceState<input1> =
       parameters: input1
       size: uint,
       digestAlgorithm: uint,
       digest: bstr
   ]

   ApplyInfo = [

   Manifest = [
       manifestVersion: uint,
       nonce: bstr,
       timestamp: uint,
       isRoot: bool,

Best Regards,

Øyvind Rønningstad
Frank Audun Kvamtrø
Krzysztof Chruściński
Andrzej Puzdrowski

Nordic Semiconductor ASA