|
| 1 | +# Copyright (c) SCITT Authors |
| 2 | +# Licensed under the MIT License. |
| 3 | +import pathlib |
| 4 | +import argparse |
| 5 | +from typing import Optional |
| 6 | + |
| 7 | +import cwt |
| 8 | +import pycose |
| 9 | +import pycose.headers |
| 10 | +import pycose.messages |
| 11 | +import pycose.keys.ec2 |
| 12 | + |
| 13 | +# TODO jwcrypto is LGPLv3, is there another option with a permissive licence? |
| 14 | +import jwcrypto.jwk |
| 15 | + |
| 16 | + |
| 17 | +@pycose.headers.CoseHeaderAttribute.register_attribute() |
| 18 | +class CWTClaims(pycose.headers.CoseHeaderAttribute): |
| 19 | + identifier = 14 |
| 20 | + fullname = "CWT_CLAIMS" |
| 21 | + |
| 22 | + |
| 23 | +@pycose.headers.CoseHeaderAttribute.register_attribute() |
| 24 | +class RegInfo(pycose.headers.CoseHeaderAttribute): |
| 25 | + identifier = 393 |
| 26 | + fullname = "REG_INFO" |
| 27 | + |
| 28 | + |
| 29 | +@pycose.headers.CoseHeaderAttribute.register_attribute() |
| 30 | +class Receipt(pycose.headers.CoseHeaderAttribute): |
| 31 | + identifier = 394 |
| 32 | + fullname = "RECEIPT" |
| 33 | + |
| 34 | + |
| 35 | +@pycose.headers.CoseHeaderAttribute.register_attribute() |
| 36 | +class TBD(pycose.headers.CoseHeaderAttribute): |
| 37 | + identifier = 395 |
| 38 | + fullname = "TBD" |
| 39 | + |
| 40 | + |
| 41 | +def create_claim( |
| 42 | + claim_path: pathlib.Path, |
| 43 | + issuer: str, |
| 44 | + subject: str, |
| 45 | + content_type: str, |
| 46 | + payload: str, |
| 47 | + private_key_pem_path: Optional[str] = None, |
| 48 | +): |
| 49 | + # https://ietf-wg-scitt.github.io/draft-ietf-scitt-architecture/draft-ietf-scitt-architecture.html#name-signed-statement-envelope |
| 50 | + |
| 51 | + # Registration Policy (label: TBD, temporary: 393): A map containing |
| 52 | + # key/value pairs set by the Issuer which are sealed on Registration and |
| 53 | + # non-opaque to the Transparency Service. The key/value pair semantics are |
| 54 | + # specified by the Issuer or are specific to the CWT_Claims iss and |
| 55 | + # CWT_Claims sub tuple. |
| 56 | + # Examples: the sequence number of signed statements |
| 57 | + # on a CWT_Claims Subject, Issuer metadata, or a reference to other |
| 58 | + # Transparent Statements (e.g., augments, replaces, new-version, CPE-for) |
| 59 | + # Reg_Info = { |
| 60 | + reg_info = { |
| 61 | + # ? "register_by": uint .within (~time), |
| 62 | + "register_by": 1000, |
| 63 | + # ? "sequence_no": uint, |
| 64 | + "sequence_no": 0, |
| 65 | + # ? "issuance_ts": uint .within (~time), |
| 66 | + "issuance_ts": 1000, |
| 67 | + # ? "no_replay": null, |
| 68 | + "no_replay": None, |
| 69 | + # * tstr => any |
| 70 | + } |
| 71 | + # } |
| 72 | + |
| 73 | + # Create COSE_Sign1 structure |
| 74 | + # https://python-cwt.readthedocs.io/en/stable/algorithms.html |
| 75 | + alg = "ES384" |
| 76 | + # Create an ad-hoc key |
| 77 | + # oct: size(int) |
| 78 | + # RSA: public_exponent(int), size(int) |
| 79 | + # EC: crv(str) (one of P-256, P-384, P-521, secp256k1) |
| 80 | + # OKP: crv(str) (one of Ed25519, Ed448, X25519, X448) |
| 81 | + key = jwcrypto.jwk.JWK() |
| 82 | + if private_key_pem_path and private_key_pem_path.exists(): |
| 83 | + key.import_from_pem(private_key_pem_path.read_bytes()) |
| 84 | + else: |
| 85 | + key = key.generate(kty="EC", crv="P-384") |
| 86 | + kid = key.thumbprint() |
| 87 | + key_as_pem_bytes = key.export_to_pem(private_key=True, password=None) |
| 88 | + # cwt_cose_key = cwt.COSEKey.generate_symmetric_key(alg=alg, kid=kid) |
| 89 | + cwt_cose_key = cwt.COSEKey.from_pem(key_as_pem_bytes, kid=kid) |
| 90 | + # cwt_cose_key_to_cose_key = cwt.algs.ec2.EC2Key.to_cose_key(cwt_cose_key) |
| 91 | + cwt_cose_key_to_cose_key = cwt_cose_key.to_dict() |
| 92 | + sign1_message_key = pycose.keys.ec2.EC2Key.from_dict(cwt_cose_key_to_cose_key) |
| 93 | + |
| 94 | + # CWT_Claims (label: 14 pending [CWT_CLAIM_COSE]): A CWT representing |
| 95 | + # the Issuer (iss) making the statement, and the Subject (sub) to |
| 96 | + # correlate a collection of statements about an Artifact. Additional |
| 97 | + # [CWT_CLAIMS] MAY be used, while iss and sub MUST be provided |
| 98 | + # CWT_Claims = { |
| 99 | + cwt_claims = { |
| 100 | + # iss (CWT_Claim Key 1): The Identifier of the signer, as a string |
| 101 | + # Example: did:web:example.com |
| 102 | + # 1 => tstr; iss, the issuer making statements, |
| 103 | + 1: issuer, |
| 104 | + # sub (CWT_Claim Key 2): The Subject to which the Statement refers, |
| 105 | + # chosen by the Issuer |
| 106 | + # Example: github.com/opensbom-generator/spdx-sbom-generator/releases/tag/v0.0.13 |
| 107 | + # 2 => tstr; sub, the subject of the statements, |
| 108 | + 2: subject, |
| 109 | + # * tstr => any |
| 110 | + } |
| 111 | + # } |
| 112 | + cwt_token = cwt.encode(cwt_claims, cwt_cose_key) |
| 113 | + |
| 114 | + # Protected_Header = { |
| 115 | + protected = { |
| 116 | + # algorithm (label: 1): Asymmetric signature algorithm used by the |
| 117 | + # Issuer of a Signed Statement, as an integer. |
| 118 | + # Example: -35 is the registered algorithm identifier for ECDSA with |
| 119 | + # SHA-384, see COSE Algorithms Registry [IANA.cose]. |
| 120 | + # 1 => int ; algorithm identifier, |
| 121 | + # https://www.iana.org/assignments/cose/cose.xhtml#algorithms |
| 122 | + # pycose.headers.Algorithm: "ES256", |
| 123 | + pycose.headers.Algorithm: getattr(cwt.enums.COSEAlgs, alg), |
| 124 | + # Key ID (label: 4): Key ID, as a bytestring |
| 125 | + # 4 => bstr ; Key ID, |
| 126 | + pycose.headers.KID: kid.encode("ascii"), |
| 127 | + # 14 => CWT_Claims ; CBOR Web Token Claims, |
| 128 | + CWTClaims: cwt_token, |
| 129 | + # 393 => Reg_Info ; Registration Policy info, |
| 130 | + RegInfo: reg_info, |
| 131 | + # 3 => tstr ; payload type |
| 132 | + pycose.headers.ContentType: content_type, |
| 133 | + } |
| 134 | + # } |
| 135 | + |
| 136 | + # Unprotected_Header = { |
| 137 | + unprotected = { |
| 138 | + # ; TBD, Labels are temporary, |
| 139 | + TBD: "TBD", |
| 140 | + # ? 394 => [+ Receipt] |
| 141 | + Receipt: None, |
| 142 | + } |
| 143 | + # } |
| 144 | + |
| 145 | + # https://github.com/TimothyClaeys/pycose/blob/e527e79b611f6cc6673bbb694056a7468c2eef75/pycose/messages/cosemessage.py#L84-L91 |
| 146 | + msg = pycose.messages.Sign1Message( |
| 147 | + phdr=protected, |
| 148 | + uhdr=unprotected, |
| 149 | + payload=payload.encode("utf-8"), |
| 150 | + ) |
| 151 | + |
| 152 | + # Sign |
| 153 | + msg.key = sign1_message_key |
| 154 | + # https://github.com/TimothyClaeys/pycose/blob/e527e79b611f6cc6673bbb694056a7468c2eef75/pycose/messages/cosemessage.py#L143 |
| 155 | + claim = msg.encode(tag=True) |
| 156 | + claim_path.write_bytes(claim) |
| 157 | + |
| 158 | + # Write out private key in PEM format if argument given and not exists |
| 159 | + if private_key_pem_path and not private_key_pem_path.exists(): |
| 160 | + private_key_pem_path.write_bytes(key_as_pem_bytes) |
| 161 | + |
| 162 | + |
| 163 | +def cli(fn): |
| 164 | + p = fn("create-claim", description="Create a fake SCITT claim") |
| 165 | + p.add_argument("--out", required=True, type=pathlib.Path) |
| 166 | + p.add_argument("--issuer", required=True, type=str) |
| 167 | + p.add_argument("--subject", required=True, type=str) |
| 168 | + p.add_argument("--content-type", required=True, type=str) |
| 169 | + p.add_argument("--payload", required=True, type=str) |
| 170 | + p.add_argument("--private-key-pem", required=False, type=pathlib.Path) |
| 171 | + p.set_defaults( |
| 172 | + func=lambda args: create_claim( |
| 173 | + args.out, |
| 174 | + args.issuer, |
| 175 | + args.subject, |
| 176 | + args.content_type, |
| 177 | + args.payload, |
| 178 | + private_key_pem_path=args.private_key_pem, |
| 179 | + ) |
| 180 | + ) |
| 181 | + |
| 182 | + return p |
| 183 | + |
| 184 | + |
| 185 | +def main(argv=None): |
| 186 | + parser = cli(argparse.ArgumentParser) |
| 187 | + args = parser.parse_args(argv) |
| 188 | + args.func(args) |
| 189 | + |
| 190 | + |
| 191 | +if __name__ == "__main__": |
| 192 | + main() |
0 commit comments