Skip to content

trestle.core.commands.sign

trestle.core.commands.sign ¤

Trestle Sign Command.

Attributes¤

logger = logging.getLogger(__name__) module-attribute ¤

Classes¤

SignCmd ¤

Bases: CommandBase


              flowchart TD
              trestle.core.commands.sign.SignCmd[SignCmd]
              trestle.core.commands.command_docs.CommandBase[CommandBase]

                              trestle.core.commands.command_docs.CommandBase --> trestle.core.commands.sign.SignCmd
                


              click trestle.core.commands.sign.SignCmd href "" "trestle.core.commands.sign.SignCmd"
              click trestle.core.commands.command_docs.CommandBase href "" "trestle.core.commands.command_docs.CommandBase"
            

Sign a JSON file as a detached DSSE provenance envelope.

This command does not modify the input JSON file. It reads the JSON bytes, canonicalizes them using RFC 8785, computes the selected digest over the canonical JSON, and records that digest in an in-toto Statement. The Statement is then signed as a DSSE payload using the provided PEM private key and written as a detached sidecar envelope.

The optional subject name is the name recorded in the in-toto Statement. If it is omitted, the input file name is used. Verification must use the same subject name when one was provided during signing.

Source code in trestle/core/commands/sign.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
class SignCmd(CommandBase):
    """Sign a JSON file as a detached DSSE provenance envelope.

    This command does not modify the input JSON file. It reads the JSON
    bytes, canonicalizes them using RFC 8785, computes the selected digest over
    the canonical JSON, and records that digest in an in-toto Statement. The
    Statement is then signed as a DSSE payload using the provided PEM private
    key and written as a detached sidecar envelope.

    The optional subject name is the name recorded in the in-toto Statement.
    If it is omitted, the input file name is used. Verification must use the
    same subject name when one was provided during signing.
    """

    name = 'sign'

    def _init_arguments(self) -> None:
        self.add_argument('-f', '--file', help='Path to the JSON file to sign.', required=True, type=pathlib.Path)
        self.add_argument(
            '--private-key',
            dest='key',
            help='Path to the PEM private key for signing.',
            required=True,
            type=pathlib.Path,
        )
        self.add_argument(
            '--key-password-env',
            help='Environment variable containing the password for an encrypted PEM private key.',
            default=None,
        )
        self.add_argument('-o', '--output', help='Output DSSE envelope file.', required=True, type=pathlib.Path)
        self.add_argument('--overwrite', help='Replace an existing DSSE envelope.', action='store_true')
        self.add_argument(
            '--digest-algorithm',
            choices=[algorithm.value for algorithm in Algorithm],
            default=DEFAULT_DIGEST_ALGORITHM.value,
            help='Artifact digest algorithm (default: SHA-256).',
        )
        self.add_argument(
            '--subject-name',
            help='Subject name to record in the in-toto Statement. Defaults to the input file name.',
            default=None,
        )

    @beta_feature('json-signing')
    def _run(self, args: argparse.Namespace) -> int:
        """Sign a JSON file."""
        try:
            log.set_log_level_from_args(args)
            self.sign(
                args.file,
                args.key,
                args.output,
                args.subject_name,
                args.key_password_env,
                args.overwrite,
                digest_algorithm=Algorithm(args.digest_algorithm),
            )
            return CmdReturnCodes.SUCCESS.value
        except Exception as e:  # pragma: no cover
            return handle_generic_command_exception(e, logger, 'Error while signing JSON')

    @classmethod
    def sign(
        cls,
        input_path: pathlib.Path,
        key_path: pathlib.Path,
        output_path: pathlib.Path,
        subject_name: Optional[str] = None,
        key_password_env: Optional[str] = None,
        overwrite: bool = False,
        digest_algorithm: Algorithm = DEFAULT_DIGEST_ALGORITHM,
    ) -> None:
        """Write a detached DSSE provenance envelope for a JSON file."""
        input_path = input_path.resolve()
        key_path = key_path.resolve()
        resolved_output_path = output_path.resolve()
        if input_path == resolved_output_path:
            raise TrestleError('DSSE output path must be different from input JSON file.')
        if key_path == resolved_output_path:
            raise TrestleError('DSSE output path must be different from private key file.')

        key_password = None
        if key_password_env is not None:
            if key_password_env not in os.environ:
                raise TrestleError(f'Key password environment variable is not set: {key_password_env}')
            key_password = os.environ[key_password_env]

        signer = load_pem_private_key_signer(key_path, key_password)
        envelope = create_oscal_provenance_envelope(input_path, signer, subject_name, digest_algorithm)
        write_dsse_envelope(envelope, output_path, overwrite)
Attributes¤
name = 'sign' class-attribute instance-attribute ¤
Methods:¤
sign(input_path, key_path, output_path, subject_name=None, key_password_env=None, overwrite=False, digest_algorithm=DEFAULT_DIGEST_ALGORITHM) classmethod ¤

Write a detached DSSE provenance envelope for a JSON file.

Source code in trestle/core/commands/sign.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
@classmethod
def sign(
    cls,
    input_path: pathlib.Path,
    key_path: pathlib.Path,
    output_path: pathlib.Path,
    subject_name: Optional[str] = None,
    key_password_env: Optional[str] = None,
    overwrite: bool = False,
    digest_algorithm: Algorithm = DEFAULT_DIGEST_ALGORITHM,
) -> None:
    """Write a detached DSSE provenance envelope for a JSON file."""
    input_path = input_path.resolve()
    key_path = key_path.resolve()
    resolved_output_path = output_path.resolve()
    if input_path == resolved_output_path:
        raise TrestleError('DSSE output path must be different from input JSON file.')
    if key_path == resolved_output_path:
        raise TrestleError('DSSE output path must be different from private key file.')

    key_password = None
    if key_password_env is not None:
        if key_password_env not in os.environ:
            raise TrestleError(f'Key password environment variable is not set: {key_password_env}')
        key_password = os.environ[key_password_env]

    signer = load_pem_private_key_signer(key_path, key_password)
    envelope = create_oscal_provenance_envelope(input_path, signer, subject_name, digest_algorithm)
    write_dsse_envelope(envelope, output_path, overwrite)

Functions:¤

handler: python