From 5b861ed4722787835cdd5e9d86efc698974f1131 Mon Sep 17 00:00:00 2001 From: steve lasker Date: Wed, 10 Jul 2024 12:52:06 -0700 Subject: [PATCH 1/3] Add create-hashed-signed-statement Signed-off-by: steve lasker --- .../create_hashed_signed_statement.py | 214 ++++++++++++++++++ scitt-scripts/entrypoint.sh | 2 +- 2 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 scitt-scripts/create_hashed_signed_statement.py diff --git a/scitt-scripts/create_hashed_signed_statement.py b/scitt-scripts/create_hashed_signed_statement.py new file mode 100644 index 0000000..5df2048 --- /dev/null +++ b/scitt-scripts/create_hashed_signed_statement.py @@ -0,0 +1,214 @@ +""" Module for creating a SCITT signed statement with a detached payload""" + +import hashlib +import argparse + +from typing import Optional + +from hashlib import sha256 + +from pycose.messages import Sign1Message +from pycose.headers import Algorithm, KID, ContentType +from pycose.algorithms import Es256 +from pycose.keys.curves import P256 +from pycose.keys.keyparam import KpKty, EC2KpD, EC2KpX, EC2KpY, KpKeyOps, EC2KpCurve +from pycose.keys.keytype import KtyEC2 +from pycose.keys.keyops import SignOp, VerifyOp +from pycose.keys import CoseKey + +from ecdsa import SigningKey, VerifyingKey + + +# CWT header label comes from version 4 of the scitt architecture document +# https://www.ietf.org/archive/id/draft-ietf-scitt-architecture-04.html#name-issuer-identity +HEADER_LABEL_CWT = 13 + +# Various CWT header labels come from: +# https://www.rfc-editor.org/rfc/rfc8392.html#section-3.1 +HEADER_LABEL_CWT_ISSUER = 1 +HEADER_LABEL_CWT_SUBJECT = 2 + +# CWT CNF header labels come from: +# https://datatracker.ietf.org/doc/html/rfc8747#name-confirmation-claim +HEADER_LABEL_CWT_CNF = 8 +HEADER_LABEL_CNF_COSE_KEY = 1 + + +# Signed Hash envelope header labels from: +# https://github.com/OR13/draft-steele-cose-hash-envelope/blob/main/draft-steele-cose-hash-envelope.md +HEADER_LABEL_PAYLOAD_HASH_ALGORITHM = 998 +HEADER_LABEL_LOCATION = 999 + + +def open_signing_key(key_file: str) -> SigningKey: + """ + opens the signing key from the key file. + NOTE: the signing key is expected to be a P-256 ecdsa key in PEM format. + While this sample script uses P-256 ecdsa, DataTrails supports any format + supported through [go-cose](https://github.com/veraison/go-cose/blob/main/algorithm.go) + """ + with open(key_file, encoding="UTF-8") as file: + signing_key = SigningKey.from_pem(file.read(), hashlib.sha256) + return signing_key + + +def open_payload(payload_file: str) -> str: + """ + opens the payload from the payload file. + """ + with open(payload_file, encoding="UTF-8") as file: + return file.read() + + +def create_hashed_signed_statement( + signing_key: SigningKey, + payload: str, + subject: str, + issuer: str, + content_type: str, + location: str, +) -> bytes: + """ + creates a hashed signed statement, given the signing_key, payload, subject and issuer + the payload will be hashed and the hash added to the payload field. + """ + + # NOTE: for the sample an ecdsa P256 key is used + verifying_key: Optional[VerifyingKey] = signing_key.verifying_key + assert verifying_key is not None + + # pub key is the x and y parts concatenated + xy_parts = verifying_key.to_string() + + # ecdsa P256 is 64 bytes + x_part = xy_parts[0:32] + y_part = xy_parts[32:64] + + # create a protected header where + # the verification key is attached to the cwt claims + protected_header = { + Algorithm: Es256, + KID: b"testkey", + ContentType: content_type, + HEADER_LABEL_CWT: { + HEADER_LABEL_CWT_ISSUER: issuer, + HEADER_LABEL_CWT_SUBJECT: subject, + HEADER_LABEL_CWT_CNF: { + HEADER_LABEL_CNF_COSE_KEY: { + KpKty: KtyEC2, + EC2KpCurve: P256, + EC2KpX: x_part, + EC2KpY: y_part, + }, + }, + }, + HEADER_LABEL_PAYLOAD_HASH_ALGORITHM: -16, # for sha256 + HEADER_LABEL_LOCATION: location, + } + + # now create a sha256 hash of the payload + # + # NOTE: any hashing algorithm can be used. + payload_hash = sha256(payload.encode("utf-8")).digest() + + # create the statement as a sign1 message using the protected header and payload + statement = Sign1Message(phdr=protected_header, payload=payload_hash) + + # create the cose_key to sign the statement using the signing key + cose_key = { + KpKty: KtyEC2, + EC2KpCurve: P256, + KpKeyOps: [SignOp, VerifyOp], + EC2KpD: signing_key.to_string(), + EC2KpX: x_part, + EC2KpY: y_part, + } + + cose_key = CoseKey.from_dict(cose_key) + statement.key = cose_key + + # sign and cbor encode the statement. + # NOTE: the encode() function performs the signing automatically + signed_statement = statement.encode([None]) + + return signed_statement + + +def main(): + """Creates a signed statement""" + + parser = argparse.ArgumentParser(description="Create a signed statement.") + + # signing key file + parser.add_argument( + "--signing-key-file", + type=str, + help="filepath to the stored ecdsa P-256 signing key, in pem format.", + default="scitt-signing-key.pem", + ) + + # payload-file (a reference to the file that will become the payload of the SCITT Statement) + parser.add_argument( + "--payload-file", + type=str, + help="filepath to the content that will be hashed into the payload of the SCITT Statement.", + default="scitt-payload.json", + ) + + # content-type + parser.add_argument( + "--content-type", + type=str, + help="The iana.org media type for the payload", + default="application/json", + ) + + # subject + parser.add_argument( + "--subject", + type=str, + help="subject to correlate statements made about an artifact.", + ) + + # issuer + parser.add_argument( + "--issuer", + type=str, + help="issuer who owns the signing key.", + ) + + # location hint + parser.add_argument( + "--location-hint", + type=str, + help="location hint for the original statement that was hashed.", + ) + + # output file + parser.add_argument( + "--output-file", + type=str, + help="name of the output file to store the signed statement.", + default="signed-statement.cbor", + ) + + args = parser.parse_args() + + signing_key = open_signing_key(args.signing_key_file) + payload = open_payload(args.payload_file) + + signed_statement = create_hashed_signed_statement( + signing_key, + payload, + args.subject, + args.issuer, + args.content_type, + args.location_hint, + ) + + with open(args.output_file, "wb") as output_file: + output_file.write(signed_statement) + + +if __name__ == "__main__": + main() diff --git a/scitt-scripts/entrypoint.sh b/scitt-scripts/entrypoint.sh index 7094279..3831a70 100755 --- a/scitt-scripts/entrypoint.sh +++ b/scitt-scripts/entrypoint.sh @@ -22,7 +22,7 @@ echo "PWD: $PWD" ls -la $TOKEN_FILE -python /scripts/create_signed_statement.py \ +python /scripts/create_hashed_signed_statement.py \ --subject ${3} \ --payload ${4} \ --content-type ${5} \ From 8d62ad10c50e3a28c7972c3eda12b29ab5f4a996 Mon Sep 17 00:00:00 2001 From: steve lasker Date: Wed, 10 Jul 2024 15:26:09 -0700 Subject: [PATCH 2/3] Cleanup: add debugging comments, change to v4 Signed-off-by: steve lasker --- README.md | 16 +++++++++++++++- scitt-scripts/entrypoint.sh | 14 ++++++++------ 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3ba8a92..657c3e5 100644 --- a/README.md +++ b/README.md @@ -98,7 +98,7 @@ jobs: - name: Register as a SCITT Signed Statement # Register the Signed Statement wit DataTrails SCITT APIs id: register-compliance-scitt-signed-statement - uses: datatrails/scitt-action@v0.4 + uses: datatrails/scitt-action@v0.5 with: datatrails-client_id: ${{ env.DATATRAILS_CLIENT_ID }} datatrails-secret: ${{ env.DATATRAILS_SECRET }} @@ -112,3 +112,17 @@ jobs: run: | rm ./signingkey.pem ``` + +## Testing Action Updates + +To test incremental changes to this github action: + +1. Fork https://github.com/datatrails/scitt-action/ into an org you own +1. Make the changes to your fork of the scitt-action +1. For the repo you wish to include this action: + - Change the `uses` to reference a branch and commit on your org/repo: + + ```yaml + uses: /scitt-action@ + uses: synsation-corp/scitt-action@5b861ed4722787835cdd5e9d86efc698974f1131 + ``` diff --git a/scitt-scripts/entrypoint.sh b/scitt-scripts/entrypoint.sh index 3831a70..277d251 100755 --- a/scitt-scripts/entrypoint.sh +++ b/scitt-scripts/entrypoint.sh @@ -1,5 +1,6 @@ #!/bin/bash -l +# Uncomment for debugging # echo "datatrails-client_id: " ${1} # echo "datatrails-secret: " ${2} # echo "subject: " ${3} @@ -14,14 +15,15 @@ SIGNED_STATEMENT_FILE=./${6} TOKEN_FILE="./bearer-token.txt" SUBJECT=${3} -# echo "Create an access token" +echo "Create an access token" /scripts/create-token.sh ${1} ${2} $TOKEN_FILE -ls -a -echo "PWD: $PWD" +# ls -a +# echo "PWD: $PWD" -ls -la $TOKEN_FILE +# ls -la $TOKEN_FILE +echo "Create a Signed Statement, hashing the payload" python /scripts/create_hashed_signed_statement.py \ --subject ${3} \ --payload ${4} \ @@ -30,7 +32,7 @@ python /scripts/create_hashed_signed_statement.py \ --signing-key-file ${8} \ --issuer ${9} -echo "SCITT Register to https://app.datatrails.ai/archivist/v1/publicscitt/entries" +echo "Register the SCITT SIgned Statement to https://app.datatrails.ai/archivist/v1/publicscitt/entries" RESPONSE=$(curl -X POST -H @$TOKEN_FILE \ --data-binary @$SIGNED_STATEMENT_FILE \ @@ -47,4 +49,4 @@ echo "OPERATION_ID: $OPERATION_ID" # RESPONSE=$(python /scripts/check_operation_status.py --operation-id $OPERATION_ID --token-file-name $TOKEN_FILE) # ENTRY_ID=$(echo $RESPONSE | jq -r .entryID) -# curl https://app.datatrails.ai/archivist/v2/publicassets/-/events?event_attributes.feed_id=$SUBJECT | jq +# curl https://app.datatrails.ai/archivist/v2/publicassets/-/events?event_attributes.subject=$SUBJECT | jq From faa28337bdbf31e6f7ab9b7493ffc2bc6bc60b8a Mon Sep 17 00:00:00 2001 From: steve lasker Date: Wed, 10 Jul 2024 15:28:56 -0700 Subject: [PATCH 3/3] Upgrade requests Signed-off-by: steve lasker --- scitt-scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scitt-scripts/requirements.txt b/scitt-scripts/requirements.txt index 79d7bb8..8156493 100644 --- a/scitt-scripts/requirements.txt +++ b/scitt-scripts/requirements.txt @@ -2,4 +2,4 @@ pycose~=1.0.1 ecdsa~=0.18.0 jwcrypto~=1.5.0 -requests~=2.31.0 \ No newline at end of file +requests>=2.32.0 \ No newline at end of file