Skip to content

Commit 802ed7f

Browse files
committed
create statement: As standalone file for rev a4645e4bc3e78ad5cfd9f8347c7e0ac8267c1079 of SCITT arch
Related: ietf-wg-scitt/draft-ietf-scitt-architecture@a4645e4 Signed-off-by: John Andersen <[email protected]>
1 parent d941d76 commit 802ed7f

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

scitt_emulator/create_statement.py

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
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()

setup.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
install_requires=[
1717
"cryptography",
1818
"cbor2",
19+
"cwt",
20+
"jwcrypto",
1921
"pycose",
2022
"httpx",
2123
"flask",

0 commit comments

Comments
 (0)