-
Notifications
You must be signed in to change notification settings - Fork 148
Update License Expression Annex #1262
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
Changes from all commits
bda37cb
7452b2a
013b67b
aabb589
7a0622c
5e9d4e9
d393c3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -2,36 +2,36 @@ | |||||
|
|
||||||
| ## Overview | ||||||
|
|
||||||
| Often a single license can be used to represent the licensing terms of a source code or binary file, but there are situations where a single license identifier is not sufficient. A common example is when software is offered under a choice of one or more licenses (e.g., GPL-2.0-only OR BSD-3-Clause). Another example is when a set of licenses is needed to represent a binary program constructed by compiling and linking two (or more) different source files each governed by different licenses (e.g., LGPL-2.1-only AND BSD-3-Clause). | ||||||
| Often a single license can be used to represent the licensing terms of a source code or binary file, but there are situations where a single license identifier is not sufficient. A common example is when software is offered under a choice of one or more licenses (e.g., `GPL-2.0-only OR BSD-3-Clause`). Another example is when a set of licenses is needed to represent a binary program constructed by compiling and linking two (or more) different source files each governed by different licenses (e.g., `LGPL-2.1-only AND BSD-3-Clause`). | ||||||
|
|
||||||
| SPDX License Expressions provide a way for one to construct expressions that more accurately represent the licensing terms typically found in open source software source code. A license expression could be a single license identifier found on the SPDX License List; a user defined license reference denoted by the `LicenseRef-(idstring)`; a license identifier combined with an SPDX exception; or some combination of license identifiers, license references and exceptions constructed using a small set of defined operators (e.g., `AND`, `OR`, `WITH` and `+`). We provide the definition of what constitutes a valid SPDX License Expression in this section. | ||||||
| SPDX License Expressions provide a way for one to construct expressions that more accurately represent the licensing terms typically found in open source software source code. A license expression could be a single license identifier found on the SPDX License List; a user defined license reference denoted by the "LicenseRef-(idstring)"; a license identifier combined with an SPDX exception; or some combination of license identifiers, license references and exceptions constructed using a small set of defined operators (e.g., "AND", "OR", "WITH" and "+"). We provide the definition of what constitutes a valid SPDX License Expression in this section. | ||||||
|
|
||||||
| The exact syntax of license expressions is described below in ABNF, as defined | ||||||
| The general format of license expressions is described below in ABNF, as defined | ||||||
| in [RFC 5234](https://datatracker.ietf.org/doc/rfc5234/) and expanded | ||||||
| in [RFC 7405](https://datatracker.ietf.org/doc/rfc7405/). | ||||||
|
|
||||||
| ```ANBF | ||||||
| ```ABNF | ||||||
| idstring = 1*(ALPHA / DIGIT / "-" / "." ) | ||||||
|
|
||||||
| license-id = <short form license identifier from SPDX License List> | ||||||
|
|
||||||
| license-exception-id = <short form license exception identifier from SPDX License List> | ||||||
|
|
||||||
| license-ref = [%s"DocumentRef-"(idstring)":"]%s"LicenseRef-"(idstring) | ||||||
| license-ref = ["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring) | ||||||
|
|
||||||
| addition-ref = [%s"DocumentRef-"(idstring)":"]%s"AdditionRef-"(idstring) | ||||||
| addition-ref = ["DocumentRef-"(idstring)":"]"AdditionRef-"(idstring) | ||||||
|
|
||||||
| simple-expression = license-id / license-id"+" / license-ref | ||||||
| simple-expression = license-id / license-id"+" / license-ref / "NONE" / "NOASSERTION" | ||||||
|
|
||||||
| addition-expression = license-exception-id / addition-ref | ||||||
|
|
||||||
| compound-expression = (simple-expression / | ||||||
|
|
||||||
| simple-expression ( %s"WITH" / %s"with" ) addition-expression / | ||||||
| simple-expression "WITH" addition-expression / | ||||||
|
|
||||||
| compound-expression ( %s"AND" / %s"and" ) compound-expression / | ||||||
| compound-expression "AND" compound-expression / | ||||||
|
|
||||||
| compound-expression ( %s"OR" / %s"or" ) compound-expression / | ||||||
| compound-expression "OR" compound-expression / | ||||||
|
|
||||||
| "(" compound-expression ")" ) | ||||||
|
|
||||||
|
|
@@ -46,48 +46,48 @@ A valid `<license-expression>` string consists of either: | |||||
|
|
||||||
| (ii) a more complex expression constructed by combining smaller valid expressions using Boolean license operators. | ||||||
|
|
||||||
| There MUST NOT be white space between a license-id and any following `+`. This supports easy parsing and backwards compatibility. There MUST be white space on either side of the operator "WITH". There MUST be white space and/or parentheses on either side of the operators `AND` and `OR`. | ||||||
| There shall not be any space between a license-id and any following "+". This supports easy parsing and backwards compatibility. | ||||||
|
|
||||||
| In the `tag:value` format, a license expression MUST be on a single line, and MUST NOT include a line break in the middle of the expression. | ||||||
| There shall be at least one space on either side of the operators "AND", "OR", and "WITH". | ||||||
|
|
||||||
| A license expression shall be on a single line, and shall not include a line break in the middle of the expression. | ||||||
goneall marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| ## Case sensitivity | ||||||
|
|
||||||
| License expression operators (`AND`, `and`, `OR`, `or`, `WITH` and `with`) should be matched in a *case-sensitive* manner, i.e., letters must be all upper case or all lower case. | ||||||
| In SPDX 3, license expressions are completely *case-insensitive*. | ||||||
|
|
||||||
| License identifiers (including license exception identifiers) used in SPDX documents or source code files should be matched in a *case-insensitive* manner. In other words, `MIT`, `Mit` and `mIt` should all be treated as the same identifier and referring to the same license. | ||||||
| That includes the operators ("AND", "OR", "WITH"), the special identifiers ("NONE" and "NOASSERTION"), as well as the license identifiers, including the user-defined ones. | ||||||
|
|
||||||
| However, please be aware that it is often important to match with the case of the canonical identifier on the [SPDX License List](https://spdx.org/licenses). This is because the canonical identifier's case is used in the URL of the license's or exception's entry on the List, and because the canonical identifier is translated to a URI in RDF documents. | ||||||
| For example, the expressions `MIT AND NOASSERTION AND (BSD-3-Clause OR LicenseRef-Name)` and `mit aNd NoaSSerTion AnD (bSd-3-clausE OR licenseref-NAME)` are equivalent. | ||||||
|
|
||||||
| For user defined license identifiers, only the variable part (after `LicenseRef-`) is case insensitive. This means, for example, that `LicenseRef-Name` and `LicenseRef-name` should be treated as the same identifier and considered to refer to the same license, while `licenseref-name` is not a valid license identifier. | ||||||
| However, please be aware that it is often important to note the case of the canonical identifier on the [SPDX License List](https://spdx.org/licenses). This is because the canonical identifier's case is used in the URL of the license's or exception's entry on the List, and because the canonical identifier is translated to a URI in RDF documents. | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| The same applies to `AdditionRef-` user defined identifiers. | ||||||
|
|
||||||
| ## Simple license expressions | ||||||
|
|
||||||
| A simple `<license-expression>` is composed one of the following: | ||||||
|
|
||||||
| - An SPDX License List Short Form Identifier. For example: `CDDL-1.0` | ||||||
| - An SPDX License List Short Form Identifier with a unary "+" operator suffix to represent the current version of the license or any later version. For example: `CDDL-1.0+` | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| - An SPDX user defined license reference: | ||||||
| - One of the special identifiers "NONE" or "NOASSERTION" | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
| - A user defined license reference: | ||||||
| `["DocumentRef-"(idstring)":"]"LicenseRef-"(idstring)`. | ||||||
| For example: | ||||||
| `LicenseRef-23`, | ||||||
| `LicenseRef-MIT-Style-1`, and | ||||||
| `DocumentRef-spdx-tool-1.2:LicenseRef-MIT-Style-2` | ||||||
|
|
||||||
| The current set of valid license identifiers can be found in [spdx.org/licenses](https://spdx.org/licenses). | ||||||
| The current set of valid license identifiers can be found in the SPDX License List. | ||||||
|
|
||||||
| ## Composite license expressions | ||||||
|
|
||||||
| ### Introduction | ||||||
|
|
||||||
| More expressive composite license expressions can be constructed using "OR", "AND", and "WITH" operators similar to constructing mathematical expressions using arithmetic operators. | ||||||
|
|
||||||
| For the `tag:value` format, any license expression that consists of more than one license identifier and/or LicenseRef, may optionally be encapsulated by parentheses: "( )". | ||||||
| Any license expression that consists of more than one license identifier and/or LicenseRef, may optionally be encapsulated by parentheses: "( )". | ||||||
|
|
||||||
| Nested parentheses can also be used to specify an order of precedence which is | ||||||
| discussed in more detail in | ||||||
| [Order of precedence and parentheses](#order-of-precedence-and-parentheses). | ||||||
| Nested parentheses can also be used to specify an order of precedence which is discussed in more detail below. | ||||||
|
|
||||||
| ### Disjunctive "OR" operator | ||||||
|
|
||||||
|
|
@@ -111,11 +111,11 @@ An example representing a choice between three different licenses would be: | |||||
| LGPL-2.1-only OR MIT OR BSD-3-Clause | ||||||
| ``` | ||||||
|
|
||||||
| It is allowed to use the operator in lower case form `or`. | ||||||
| The special identifiers "NONE" or "NOASSERTION" shall not be used with the "OR" operator. | ||||||
|
|
||||||
| ### Conjunctive "AND" operator | ||||||
|
|
||||||
| If required to simultaneously comply with two or more licenses, use the conjunctive binary "AND" operator to construct a new license expression, where both the left and right operands are a valid license expression values. | ||||||
| If required to simultaneously comply with two or more licenses, use the conjunctive binary "AND" operator to construct a new license expression, where both the left and right operands are valid license expression values. | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| For example, when one is required to comply with both the LGPL-2.1-only and MIT licenses, a valid expression would be: | ||||||
|
|
||||||
|
|
@@ -129,13 +129,13 @@ The "AND" operator is commutative, meaning that the above expression should be c | |||||
| MIT AND LGPL-2.1-only | ||||||
| ``` | ||||||
|
|
||||||
| An example where all three different licenses apply would be: | ||||||
| An example where three different licenses apply would be: | ||||||
|
|
||||||
| ```text | ||||||
| LGPL-2.1-only AND MIT AND BSD-2-Clause | ||||||
| ``` | ||||||
|
|
||||||
| It is allowed to use the operator in lower case form `and`. | ||||||
| The "AND" operator is the only operator that can be used in conjuction with the special identifiers "NONE" or "NOASSERTION". | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
goneall marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| ### Additive "WITH" operator | ||||||
|
|
||||||
|
|
@@ -153,7 +153,7 @@ GPL-2.0-or-later WITH Bison-exception-2.2 | |||||
|
|
||||||
| The current set of valid license exceptions identifiers can be found in [spdx.org/licenses](https://spdx.org/licenses). | ||||||
|
|
||||||
| It is allowed to use the operator in lower case form `with`. | ||||||
| The special identifiers "NONE" or "NOASSERTION" shall not be used with the "WITH" operator. | ||||||
|
|
||||||
| ### Order of precedence and parentheses | ||||||
|
|
||||||
|
|
@@ -174,86 +174,74 @@ For example, the following expression: | |||||
| LGPL-2.1-only OR BSD-3-Clause AND MIT | ||||||
| ``` | ||||||
|
|
||||||
| represents a license choice between either LGPL-2.1-only or the expression "BSD-3-Clause AND MIT" because the AND operator takes precedence over (is applied before) the OR operator. | ||||||
| represents a license choice between either LGPL-2.1-only or the expression "BSD-3-Clause AND MIT" because the "AND" operator takes precedence over (is applied before) the "OR" operator. | ||||||
|
|
||||||
| When required to express an order of precedence that is different from the default order a `<license-expression>` can be encapsulated in pairs of parentheses: ( ), to indicate that the operators found inside the parentheses takes precedence over operators outside. This is also similar to the use of parentheses in an algebraic expression e.g., (5+7)/2. | ||||||
|
|
||||||
| For instance, the following expression: | ||||||
|
|
||||||
| ```text | ||||||
| MIT AND (LGPL-2.1-or-later OR BSD-3-Clause) | ||||||
| (LGPL-2.1-or-later OR BSD-3-Clause) AND MIT | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
I think the original author of this example purposefully put
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But this reorders the terms from the previous example. The idea was to show and explain the expression without parentheses and then say "if you want to do the other way, add parentheses". |
||||||
| ``` | ||||||
|
|
||||||
| states the OR operator should be applied before the AND operator. That is, one should first select between the LGPL-2.1-or-later or the BSD-3-Clause license before applying the MIT license. | ||||||
|
|
||||||
| ### License expressions in RDF | ||||||
|
|
||||||
| A conjunctive license can be expressed in RDF via a `<spdx:ConjunctiveLicenseSet>` element, with an spdx:member property for each element in the conjunctive license. Two or more members are required. | ||||||
|
|
||||||
| ```XML | ||||||
| <spdx:ConjunctiveLicenseSet> | ||||||
| <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-only"/> | ||||||
| <spdx:ExtractedLicensingInfo rdf:about | ||||||
| ="http://example.org#LicenseRef-EternalSurrender"> | ||||||
| <spdx:extractedText> | ||||||
| In exchange for using this software, you agree to give | ||||||
| its author all your worldly possessions. You will not | ||||||
| hold the author liable for all the damage this software | ||||||
| will inevitably cause not only to your person and | ||||||
| property, but to the entire fabric of the cosmos. | ||||||
| </spdx:extractedText> | ||||||
| <spdx:licenseId>LicenseRef-EternalSurrender</spdx:licenseId> | ||||||
| </spdx:ExtractedLicensingInfo> | ||||||
| </spdx:ConjunctiveLicenseSet> | ||||||
| ``` | ||||||
| states the "OR" operator should be applied before the "AND" operator. That is, one should first select between the LGPL-2.1-or-later or the BSD-3-Clause license before applying the MIT license. | ||||||
|
|
||||||
| A disjunctive license can be expressed in RDF via a `<spdx:DisjunctiveLicenseSet>` element, with an spdx:member property for each element in the disjunctive license. Two or more members are required. | ||||||
|
|
||||||
| ```XML | ||||||
| <spdx:DisjunctiveLicenseSet> | ||||||
| <spdx:member rdf:resource="http://spdx.org/licenses/GPL-2.0-only"/> | ||||||
| <spdx:member> | ||||||
| <spdx:ExtractedLicensingInfo rdf:about | ||||||
| ="http://example.org#LicenseRef-EternalSurrender"> | ||||||
| <spdx:extractedText> | ||||||
| In exchange for using this software, you agree to | ||||||
| give its author all your worldly possessions. You | ||||||
| will not hold the author liable for all the damage | ||||||
| this software will inevitably cause not only to | ||||||
| your person and property, but to the entire fabric | ||||||
| of the cosmos. | ||||||
| </spdx:extractedText> | ||||||
| <spdx:licenseId>LicenseRef-EternalSurrender</spdx:licenseId> | ||||||
| </spdx:ExtractedLicensingInfo> | ||||||
| </spdx:member> | ||||||
| </spdx:DisjunctiveLicenseSet> | ||||||
| ``` | ||||||
| ## Complete grammar | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| The complete syntax of license expressions, | ||||||
| including precedence and whitespace, | ||||||
| is described by the following ABNF: | ||||||
|
|
||||||
| ```ABNF | ||||||
| ; ABNF Grammar for License Expressions | ||||||
|
|
||||||
| SPSX-license-expression = (or-operand *( required-ws "OR" required-ws or-operand )) / special-identifier | ||||||
zvr marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
|
|
||||||
| or-operand = (term required-ws "AND" required-ws term *( required-ws "AND" required-ws term )) / base-term | ||||||
|
|
||||||
| term = base-term / special-identifier | ||||||
|
|
||||||
| base-term = with-expression / identifier / parenthesized-expression | ||||||
|
|
||||||
| with-expression = identifier required-ws "WITH" required-ws addition-identifier | ||||||
|
|
||||||
| addition-identifier = license-exception-id / addition-ref | ||||||
|
|
||||||
| identifier = license-id / or-later-expression / license-ref | ||||||
|
|
||||||
| or-later-expression = license-id PLUS | ||||||
|
|
||||||
| parenthesized-expression = LPAREN optional-ws expression optional-ws RPAREN | ||||||
|
|
||||||
| special-identifier = "NONE" / "NOASSERTION" | ||||||
|
|
||||||
| ; --- SPDX License List contents --- | ||||||
|
|
||||||
| license-id = <short form license identifier from SPDX License List> | ||||||
| license-exception-id = <short form license exception identifier from SPDX License List> | ||||||
|
|
||||||
| ; --- User-defined identifiers --- | ||||||
|
|
||||||
| license-ref = [ "DocumentRef-" idstring ":" ] "LicenseRef-" idstring | ||||||
| addition-ref = [ "DocumentRef-" idstring ":" ] "AdditionRef-" idstring | ||||||
|
|
||||||
| idstring = *id-char alnum *id-char | ||||||
| idchar = alnum / DOT / DASH | ||||||
| alnum = ALPHA / DIGIT | ||||||
|
|
||||||
| ; --- Whitespace and characters --- | ||||||
|
|
||||||
| optional-ws = *SPACE ; Optional whitespace (zero or more spaces) | ||||||
| required-ws = 1*SPACE ; Required whitespace (one or more spaces) | ||||||
|
|
||||||
| SPACE = %x20 ; Space character | ||||||
| LPAREN = %x28 ; ( - Left parenthesis | ||||||
| RPAREN = %x29 ; ) - Right parenthesis | ||||||
| PLUS = %2B ; + - Plus | ||||||
| DASH = %2D ; - - Dash, hyphen | ||||||
| DOT = %2E ; . - Dot, fullstop, period | ||||||
|
|
||||||
| ALPHA = %x41-5A / %x61-7A ; A-Z / a-z | ||||||
| DIGIT = %x30-39 ; 0-9 | ||||||
|
|
||||||
| A License Exception can be expressed in RDF via a `<spdx:LicenseException>` element. This element has the following unique mandatory (unless specified otherwise) attributes: | ||||||
|
|
||||||
| - `comment` - An `rdfs:comment` element describing the nature of the exception. | ||||||
| - `seeAlso` (optional, one or more)- An `rdfs:seeAlso` element referencing external sources of information on the exception. | ||||||
| - `example` (optional) - Text describing examples of this exception. | ||||||
| - `name` - The full human readable name of the item. | ||||||
| - `licenseExceptionId` - The identifier of an exception in the SPDX License List to which the exception applies. | ||||||
| - `licenseExceptionText` - Full text of the license exception. | ||||||
|
|
||||||
| ```XML | ||||||
| <rdf:Description rdf:about | ||||||
| ="http://example.org#SPDXRef-ButIDontWantToException"> | ||||||
| <rdfs:comment>This exception may be invalid in some | ||||||
| jurisdictions.</rdfs:comment> | ||||||
| <rdfs:seeAlso>http://dilbert.com/strip/1997-01-15</rdfs:seeAlso> | ||||||
| <spdx:example>So this one time, I had a license exception | ||||||
| …</spdx:example> | ||||||
| <spdx:licenseExceptionText> | ||||||
| A user of this software may decline to follow any subset of | ||||||
| the terms of this license upon finding any or all such terms | ||||||
| unfavorable. | ||||||
| </spdx:licenseExceptionText> | ||||||
| <spdx:name>"But I Don't Want To" Exception</spdx:name> | ||||||
| <spdx:licenseExceptionId>SPDXRef-ButIDontWantToException</spdx:licenseExceptionId> | ||||||
| <rdf:type rdf:resource | ||||||
| ="http://spdx.org/rdf/terms#LicenseException"/> | ||||||
| </rdf:Description> | ||||||
| ``` | ||||||
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.
My understanding (and please correct me if I'm wrong!) is that @JPEWdev's original suggestion was to add "NONE" and "NOASSERTION" as actual license identifiers to the license list (including all associated metadata for them). If that's the solution, then adding
NONEandNOASSERTIONas "special cased"simple-expressionvalues is redundant - they'd be covered automatically as any otherlicense-id.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 don't know what the "original suggestion" was, but I have never heard of adding "NONE" and "NOASSERTION" to the SPDX License List.
I am opposed to this (as they are not licenses), and I am pretty confident the Legal Team will also disagree. Such an addition would also require changes to the model, deletion of the corresponding Individuals, etc.
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.
IIRC it was more of a suggestion than anything. The annoyance with them not being actual "real" licenses is that all code will need to have special cases to deal with them. It's not impossible, just annoying
Uh oh!
There was an error while loading. Please reload this page.
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.
+1 to what @JPEWdev said - not having these in the license list is more of an irritation than a showstopper. But that said, without additional machine-readable metadata (somewhere) these values appearing in an SPDX license expression will be difficult for a reader unfamiliar with SPDX to interpret, given that each tool will likely present this information in different and somewhat arbitrary ways. Given that in large (transitive) SBOMs these values will be almost ubiquitous, it seems unwise to me to special case / hard code them as is implemented here.
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.
We had some discussion on this during a code review of a Java library:
(click to "show resolved" there to see more of @pmonks and my comments)
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.
In license expressions serialized as strings, they are two special identifiers, as they have been for around 15 years.
In actual linked data, they are definitely defined as individual elements: NONE and NOASSERTION.
Uh oh!
There was an error while loading. Please reload this page.
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.
@zvr until recently
NONEandNOASSERTIONwere not part of the SPDX *license expression specification, notwithstanding their use by certain tooling. I think it's a mistake to blindly adopt the semantics that those tools chose, without thinking more broadly about how it should be implemented in a formal manner.Furthermore, independent license expressions serialised as strings without any supporting model is a common occurrence, especially in ecosystems (such as the Java / Maven ecosystem, one of the largest extant package ecosystems) that don't support SPDX directly. To me, supporting SPDX tool developers who operate in those ecosystems has merit, whatever one might think of those ecosystems and their lack of direct SPDX support.
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.
Just to clarify,
NONEandNOASSERTIONhave been present as SPDX licensing values since the original v1.0 was released in 2010/2011. See https://spdx.dev/wp-content/uploads/sites/31/2023/09/spdx-1.0.pdf, section 4.12 for example.