Skip to content

JSFSignature cannot be unmarshalled: inline signatures are dropped and .Algorithm panics #280

Description

@rexx4314

Description

JSFSignature embeds *JSFSigner with a json:"-" tag:

// cyclonedx.go
type JSFSignature struct {
	*JSFSigner `json:"-" xml:"-"`

	Signers *[]JSFSigner `json:"signers,omitempty" xml:"-"`
	Chain   *[]JSFSigner `json:"chain,omitempty" xml:"-"`
}

Because the embedded pointer is excluded from JSON, it is never populated during decoding. This has three consequences:

  1. A single inline signature is discarded. {"signature": {"algorithm": "RS512", "value": "..."}} — the form used in the specification's own example — decodes to a non-nil Signature whose fields are all empty.
  2. A round trip destroys it. Decoding and re-encoding a BOM with an inline signature produces output with no signature at all, so any tool that reads and rewrites a signed BOM silently strips the signature.
  3. Reading Signature.Algorithm panics for every signature form. The promoted field resolves through the nil embedded pointer. This is not limited to the inline case: signers and chain documents also leave the embedded struct nil, so the panic happens there too.

Reproducer

package main

import (
	"bytes"
	"fmt"

	cdx "github.com/CycloneDX/cyclonedx-go"
)

func main() {
	const doc = `{"bomFormat":"CycloneDX","specVersion":"1.6","version":1,` +
		`"signature":{"algorithm":"RS512","value":"abc"}}`

	var bom cdx.BOM
	if err := cdx.NewBOMDecoder(bytes.NewReader([]byte(doc)), cdx.BOMFileFormatJSON).Decode(&bom); err != nil {
		panic(err)
	}

	fmt.Println("Signature != nil :", bom.Signature != nil)   // true
	fmt.Println("JSFSigner != nil :", bom.Signature.JSFSigner != nil) // false — data was dropped

	// Round trip loses the signature entirely:
	var out bytes.Buffer
	_ = cdx.NewBOMEncoder(&out, cdx.BOMFileFormatJSON).Encode(&bom)
	fmt.Println("round trip keeps algorithm:", bytes.Contains(out.Bytes(), []byte("algorithm"))) // false

	fmt.Println(bom.Signature.Algorithm) // panic: nil pointer dereference
}

Output:

Signature != nil : true
JSFSigner != nil : false
round trip keeps algorithm: false
panic: runtime error: invalid memory address or nil pointer dereference

Substituting "signature":{"signers":[{"algorithm":"RS512","value":"abc"}]} keeps the data in Signers, but the final line still panics.

Impact

Consumers cannot read a signature through the public API without either crashing or bypassing the library. Two projects have independently worked around it by parsing the raw JSON — sbomqs carries the comment "Since cyclonedx-go doesn't properly unmarshal signatures yet, we need to parse it manually from the raw JSON", and I did the same in my own tool before tracing it here.

Suggested fix

JSF allows a block to be either a single signer or a set (signers / chain), so a custom UnmarshalJSON on JSFSignature seems like the smallest change that keeps the current field layout: decode into the embedded JSFSigner when algorithm is present at the top level, and into Signers/Chain otherwise. A matching MarshalJSON would fix the round trip.

If keeping the embedded pointer is awkward, an alternative is to make the single-signer fields explicit on JSFSignature rather than promoted, which also removes the nil-dereference footgun.

Happy to open a PR if the maintainers prefer one of these shapes.

Environment

cyclonedx-go v0.11.0 (latest release) with go1.26.5. The struct definition on master (cyclonedx.go:909, checked 2026-08-07) is identical, so this is not fixed in an unreleased commit.

Related

#17 tracks implementing JSF signing/verification as a feature. This report is about the existing struct being unusable for reading a signature that is already there — I believe they are separate, but happy to fold this in if you see it as part of the same work.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions