The component descriptor contains several kinds of information:
- volatile label settings, which might be changeable.
- artifact access information, which might be changed during transport steps.
- static information describing the features and artifacts of a component version.
The digest of a component descriptor is calculated on a normalized form of the elements of the component descriptor. The normalized form contains only the signature relevant information, everything else gets removed during the normalization process. The resulting string is the source for calculating the digest. This digest is then finally signed (and verified).
A normalized component descriptor is a subset of its elements containing only the properties relevant for signing:
- based on JSON
- map serializes as alphanumerically ordered list of fields (to define unique order)
- field is map with two keys 'name', 'value'
Like for signature algorithms, the model offers the possibility to work with different normalization algorithms and formats.
The algorithms used for normalization are listed in the extensible parts of the specification.
A component descriptor contains static information and information, which may change over time, e.g. access method specifications might be changed during transport steps. A digest should be stable even after a transport and therefore should only hash static information. Therefore, a component descriptor is transformed into a format that only contains immutable fields, finally relevant for the signing process and assuring data integrity.
Relevant fields and their mapping to the normalized data structure for JsonNormalisationV2
are:
- Component Name: mapped to
component.name
- Component Version: mapped to
component.version
- Component Labels: mapped to
component.labels
(see Labels]) - Component Provider: mapped to
component.provider
- Resources: mapped to
component.resources
, always empty list enforced, without the source references (see Labels] and Access Methods]) - Sources: mapped to
component.sources
, always empty list enforced, (see Labels] and Access Methods]) - References: mapped to
component.references
, always empty list enforced, (see Labels])
Access method specifications are completely ignored.
A resource or source is ignored, if the access method type is none
.
Labels by default are removed before signing, but can be marked with a special boolean
property signing
. This property indicates that the label is
signing-relevant and therefore becomes part of the digest. As a consequence such
labels cannot be changed during the lifecycle of a component version anymore
and should only describe static information.
The structure of signing-relevant labels is preserved from the component
descriptor version v2
.
Example:
labels:
- name: label1
value: foo
- name: label2
value: bar
signing: true
label1
will be excluded from the digest, whereas label2
will be included.
The value of any label is taken as is, preserving a potentially deeply nested structure.
If a resource should not be part of the normalization and later signing, the resource needs a special digest in the following format:
digest:
hashAlgorithm: NO-DIGEST
normalisationAlgorithm: EXCLUDE-FROM-SIGNATURE
value: NO-DIGEST
The generic format is based on a data structure consisting of dictionaries, lists and simple values, like strings and integers.
The signing relevant information described by a component descriptor is mapped to such a data structure according to the format specifications described below.
This data structure is mapped to a formal JSON representation, which only contains clearly ordered elements. It is marshalled without whitespaces contained in the representation. The resulting byte stream is directly defined by the inbound data structure and independent of the order of marshalling dictionaries/objects. Its digest can be used as basis for calculating a signature.
To map lists and dictionaries into such clearly ordered elements the rules below are used. The inbound data structures in the examples are shown in YAML notation.
Simple values are kept as they are.
Example:
"bob"
will result in :
"bob"
All dictionaries are converted into lists where each element is a single-entry dictionary containing the key/value pair of the original entry. This list is ordered by lexicographical order of the keys.
Example:
bob: 26
alice: 25
will result in :
[{"alice":25},{"bob":26}]
The values are converted according to the same rules, recursively.
Example:
people:
bob: 26
alice: 25
will result in :
[{"people":[{"alice":25},{"bob":26}]}]
Lists are converted into JSON arrays and preserve the order of the elements.
Example:
- bob
- alice
normalized to:
["bob","alice"]
The values are converted according to the same rules, recursively.
Example:
- bob: 26
- alice: 25
will result in :
[[{"bob":26}],[{"alice":25}]]
The following snippet is taken from a real component descriptor.
resources:
- access:
localReference: blob
mediaType: text/plain
referenceName: ref
type: localBlob
extraIdentity:
additional: value
other: othervalue
name: elem1
relation: local
type: elemtype
version: 1
This will be normalized to
[{"resources":[[{"access":[{"localReference":"blob"},{"mediaType":"text/plain"},{"referenceName":"ref"},{"type":"localBlob"}]},{"extraIdentity":[{"additional":"value"},{"other":"othervalue"}]},{"name":"elem1"},{"relation":"local"},{"type":"elemtype"},{"version":1}]]}]
Formatted with whitespaces for better readability it looks like:
[
{
"resources": [
[
{
"access": [
{
"localReference": "blob"
},
{
"mediaType": "text/plain"
},
{
"referenceName": "ref"
},
{
"type": "localBlob"
}
]
},
{
"extraIdentity": [
{
"additional": "value"
},
{
"other": "othervalue"
}
]
},
{
"name": "elem1"
},
{
"relation": "local"
},
{
"type": "elemtype"
},
{
"version": 1
}
]
]
}
]
Empty lists are normalized as empty lists
myList: []
[{"myList":[]}]
Null values are skipped during initialization
myList: ~
myList: null
myList:
and are all normalized to:
[]