Skip to content

Commit 1d4e4c1

Browse files
authored
feat: Update to use latest c2pa-python SDK
feat: Update to use latest c2pa-python SDK
2 parents 6e47bce + 0e6ace4 commit 1d4e4c1

File tree

8 files changed

+209
-214
lines changed

8 files changed

+209
-214
lines changed

app.py

Lines changed: 61 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,10 @@
1919
import boto3
2020
import base64
2121
from flask_cors import CORS
22-
from c2pa import *
23-
from hashlib import sha256
22+
from c2pa import Builder, C2paSigningAlg, Signer
23+
from cryptography.hazmat.primitives import hashes, serialization
24+
from cryptography.hazmat.primitives.asymmetric import ec
25+
from cryptography.hazmat.backends import default_backend
2426

2527

2628
# Load environment variable from .env file
@@ -45,33 +47,38 @@
4547
# By default, env vars with the `FLASK_`` prefix
4648
# app.config.from_prefixed_env()
4749

50+
# Declare global variables
51+
global private_key, kms, kms_key_id
52+
4853
private_key = None
54+
kms = None
55+
kms_key_id = None
4956

5057
if 'USE_LOCAL_KEYS' in app_config and app_config['USE_LOCAL_KEYS'] == 'True':
5158
# local test certs for development (and test client)
5259
print('Using local test certs for signing')
5360

54-
env_ps256_pem_path = os.environ.get('PS256_PEM_PATH_PYTHON_EXAMPLE')
61+
env_es256_pem_path = os.environ.get('ES256_PEM_PATH_PYTHON_EXAMPLE')
5562
env_cert_chain_path = os.environ.get('CERT_CHAIN_PATH_PYTHON_EXAMPLE')
56-
ps256_pem_path = 'tests/test-certs/ps256.pem'
57-
cert_chain_path = 'tests/test-certs/ps256.pub'
63+
es256_pem_path = 'tests/test-certs/es256_private.key'
64+
cert_chain_path = 'tests/test-certs/es256_certs.pem'
5865

59-
if env_ps256_pem_path is not None and env_cert_chain_path is not None:
60-
print(f"Using certificates pointed to by env variables {env_ps256_pem_path} and {env_cert_chain_path}")
61-
ps256_pem_path = env_ps256_pem_path
66+
if env_es256_pem_path is not None and env_cert_chain_path is not None:
67+
print(f"Using certificates pointed to by env variables {env_es256_pem_path} and {env_cert_chain_path}")
68+
es256_pem_path = env_es256_pem_path
6269
cert_chain_path = env_cert_chain_path
63-
elif os.path.exists(ps256_pem_path) and os.path.exists(cert_chain_path):
64-
print(f"Using certificates added locally to this repo {ps256_pem_path} and {cert_chain_path}")
70+
elif os.path.exists(es256_pem_path) and os.path.exists(cert_chain_path):
71+
print(f"Using certificates added locally to this repo {es256_pem_path} and {cert_chain_path}")
6572
else:
6673
print("Using provided default test certificates and certificate chain")
67-
ps256_pem_path = 'tests/certs/ps256.pem'
68-
cert_chain_path = 'tests/certs/ps256.pub'
74+
es256_pem_path = 'tests/certs/es256_private.key'
75+
cert_chain_path = 'tests/certs/es256_certs.pem'
6976

70-
private_key = open(ps256_pem_path, 'rb').read()
77+
private_key = open(es256_pem_path, 'rb').read()
7178
cert_chain = open(cert_chain_path, 'rb').read()
7279

7380
encoded_cert_chain = base64.b64encode(cert_chain).decode('utf-8')
74-
signing_alg_str = 'PS256'
81+
signing_alg_str = 'ES256'
7582
else:
7683
print('Using KMS for signing')
7784

@@ -108,17 +115,18 @@
108115
if 'TIMESTAMP_URL' in app_config and app_config['TIMESTAMP_URL']:
109116
timestamp_url = app_config['TIMESTAMP_URL']
110117
else:
111-
timestamp_url = 'http://timestamp.digicert.com' # Default timestamp URL (change to None later?)
118+
# Default timestamp URL (change to None later?)
119+
timestamp_url = 'http://timestamp.digicert.com'
112120

113121
# TODO: Get signing_alg_str from env when we support more algorithms
114122
try:
115-
signing_alg = getattr(SigningAlg, signing_alg_str)
123+
signing_alg = getattr(C2paSigningAlg, signing_alg_str)
116124
except AttributeError:
117125
raise ValueError(f"Unsupported signing algorithm: {signing_alg_str}")
118126

119127

120128
@app.route("/attach", methods=["POST"])
121-
def resize():
129+
def attach_sign_image():
122130
"""Gets a JPEG image to sign and returns the signed JPEG image"""
123131

124132
request_data = request.get_data()
@@ -139,11 +147,12 @@ def resize():
139147
"data": {
140148
"actions": [
141149
{
142-
"action": "c2pa.edited",
150+
"action": "c2pa.created",
143151
"softwareAgent": {
144152
"name": "C2PA Python Example",
145153
"version": "0.1.0"
146-
}
154+
},
155+
"digitalSourceType": "http://cv.iptc.org/newscodes/digitalsourcetype/digitalCreation"
147156
}
148157
]
149158
}
@@ -152,26 +161,45 @@ def resize():
152161
})
153162

154163
try:
155-
builder = Builder(manifest)
156-
157-
signer = create_signer(kms_sign, signing_alg,
158-
cert_chain, timestamp_url)
159-
160-
result = io.BytesIO(b"")
161-
builder.sign(signer, content_type, io.BytesIO(request_data), result)
162-
163-
return result.getvalue()
164+
with Builder(manifest) as builder:
165+
# Create signer using the new API
166+
callback_func = es256_sign if private_key is not None else kms_sign
167+
signer = Signer.from_callback(
168+
callback=callback_func,
169+
alg=signing_alg,
170+
certs=cert_chain,
171+
tsa_url=timestamp_url
172+
)
173+
174+
result = io.BytesIO(b"")
175+
builder.sign(signer, content_type, io.BytesIO(request_data), result)
176+
177+
return result.getvalue()
164178
except Exception as e:
165179
logging.error(e)
166180
abort(500, description=e)
167181

168182

183+
# Uses ES256 alg to sign
184+
def es256_sign(data: bytes) -> bytes:
185+
"""Signs the data using ES256 algorithm with the private key"""
186+
private_key_obj = serialization.load_pem_private_key(
187+
private_key,
188+
password=None,
189+
backend=default_backend()
190+
)
191+
signature = private_key_obj.sign(
192+
data,
193+
ec.ECDSA(hashes.SHA256())
194+
)
195+
return signature
196+
197+
169198
# Uses KMS to sign
170199
def kms_sign(data: bytes) -> bytes:
171200
"""Signs the data using a KMS key id"""
172-
173-
hashed_data = sha256(data).digest()
174-
return kms.sign(KeyId=kms_key_id, Message=hashed_data, MessageType="DIGEST", SigningAlgorithm="ECDSA_SHA_256")["Signature"]
201+
result = kms.sign(KeyId=kms_key_id, Message=data, MessageType="RAW", SigningAlgorithm="ECDSA_SHA_256")["Signature"]
202+
return result
175203

176204

177205
@app.route("/health", methods=["GET"])
@@ -208,8 +236,8 @@ def sign():
208236
try:
209237
data = request.get_data()
210238
if private_key is not None:
211-
print('Using sign_ps256')
212-
return sign_ps256(data, private_key)
239+
print('Using es256_sign')
240+
return es256_sign(data)
213241
else:
214242
print('Using kms_sign')
215243
return kms_sign(data)

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
c2pa-python==0.6.1
1+
c2pa-python>=0.25.0
22
boto3
33
pyyaml
44
pyasn1==0.4.8

tests/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# C2PA Python example client
2+
3+
NOTE: This documentation is for the example developer.
4+
5+
This directory contains the client for signing JPEG images with Content Credentials. The client works with the C2PA Python example server in development mode using demo certificates (included in this repository).
6+
7+
## Overview
8+
9+
The `client.py` file is a command-line test tool that signs JPEG image files. It connects to the signing server defined in `app.py` to add Content Credentials to JPEG images.
10+
11+
## Prerequisites
12+
13+
1. Install Python dependencies. Enter this command in the top level of the repo:
14+
15+
```bash
16+
pip install -r requirements.txt
17+
```
18+
19+
2. Run the signing server (`app.py`). It uses local certificates and must be accessible to the client:
20+
21+
```bash
22+
python app.py
23+
```
24+
25+
3. Run the client as explained below to submit images for the server to sign.
26+
27+
## Usage
28+
29+
NOTE: This test client supports only JPEG images.
30+
31+
### Basic command
32+
33+
```bash
34+
python tests/client.py <image-file> -o <output-directory>
35+
```
36+
37+
### Command line arguments
38+
39+
| Argument | Type | Required | Description |
40+
|----------|------|----------|-------------|
41+
| `files` | string | Yes | One or more image files to be signed |
42+
| `-o, --output` | string | Yes | Output directory where signed images will be saved |
43+
| `-f, --envfile` | string | No | Path to environment configuration file |
44+
45+
### Examples
46+
47+
#### Sign a single image
48+
49+
```bash
50+
python tests/client.py ./image-to-sign.jpeg -o signed-images
51+
```
52+
53+
#### Use custom configuration
54+
55+
```bash
56+
python tests/client.py ./image-to-sign.jpeg -o signed-images -f ./my-config.env
57+
```
58+
59+
## Signing flow when using the client
60+
61+
1. **Server Connection**: Client connects to the signing server's `/signer_data` endpoint.
62+
2. **Configuration Retrieval**: Gets signing algorithm, certificate chain, and signing URL.
63+
3. **Signer Creation**: Creates a remote signer using the modern C2PA API (`Signer.from_callback`)/
64+
4. **Manifest Creation**: Generates a default C2PA manifest.
65+
5. **Image Processing**: Creates a thumbnail for the manifest and adds it as resource.
66+
6. **Remote Signing**: Uses the `Builder.sign()` method with remote signing callback.
67+
7. **Output**: Saves the signed image to the specified output directory.

tests/certs/es256_certs.pem

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIChzCCAi6gAwIBAgIUcCTmJHYF8dZfG0d1UdT6/LXtkeYwCgYIKoZIzj0EAwIw
3+
gYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJU29tZXdoZXJl
4+
MScwJQYDVQQKDB5DMlBBIFRlc3QgSW50ZXJtZWRpYXRlIFJvb3QgQ0ExGTAXBgNV
5+
BAsMEEZPUiBURVNUSU5HX09OTFkxGDAWBgNVBAMMD0ludGVybWVkaWF0ZSBDQTAe
6+
Fw0yMjA2MTAxODQ2NDBaFw0zMDA4MjYxODQ2NDBaMIGAMQswCQYDVQQGEwJVUzEL
7+
MAkGA1UECAwCQ0ExEjAQBgNVBAcMCVNvbWV3aGVyZTEfMB0GA1UECgwWQzJQQSBU
8+
ZXN0IFNpZ25pbmcgQ2VydDEZMBcGA1UECwwQRk9SIFRFU1RJTkdfT05MWTEUMBIG
9+
A1UEAwwLQzJQQSBTaWduZXIwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQPaL6R
10+
kAkYkKU4+IryBSYxJM3h77sFiMrbvbI8fG7w2Bbl9otNG/cch3DAw5rGAPV7NWky
11+
l3QGuV/wt0MrAPDoo3gwdjAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQMMAoGCCsG
12+
AQUFBwMEMA4GA1UdDwEB/wQEAwIGwDAdBgNVHQ4EFgQUFznP0y83joiNOCedQkxT
13+
tAMyNcowHwYDVR0jBBgwFoAUDnyNcma/osnlAJTvtW6A4rYOL2swCgYIKoZIzj0E
14+
AwIDRwAwRAIgOY/2szXjslg/MyJFZ2y7OH8giPYTsvS7UPRP9GI9NgICIDQPMKrE
15+
LQUJEtipZ0TqvI/4mieoyRCeIiQtyuS0LACz
16+
-----END CERTIFICATE-----
17+
-----BEGIN CERTIFICATE-----
18+
MIICajCCAg+gAwIBAgIUfXDXHH+6GtA2QEBX2IvJ2YnGMnUwCgYIKoZIzj0EAwIw
19+
dzELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRIwEAYDVQQHDAlTb21ld2hlcmUx
20+
GjAYBgNVBAoMEUMyUEEgVGVzdCBSb290IENBMRkwFwYDVQQLDBBGT1IgVEVTVElO
21+
R19PTkxZMRAwDgYDVQQDDAdSb290IENBMB4XDTIyMDYxMDE4NDY0MFoXDTMwMDgy
22+
NzE4NDY0MFowgYwxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJ
23+
U29tZXdoZXJlMScwJQYDVQQKDB5DMlBBIFRlc3QgSW50ZXJtZWRpYXRlIFJvb3Qg
24+
Q0ExGTAXBgNVBAsMEEZPUiBURVNUSU5HX09OTFkxGDAWBgNVBAMMD0ludGVybWVk
25+
aWF0ZSBDQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABHllI4O7a0EkpTYAWfPM
26+
D6Rnfk9iqhEmCQKMOR6J47Rvh2GGjUw4CS+aLT89ySukPTnzGsMQ4jK9d3V4Aq4Q
27+
LsOjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW
28+
BBQOfI1yZr+iyeUAlO+1boDitg4vazAfBgNVHSMEGDAWgBRembiG4Xgb2VcVWnUA
29+
UrYpDsuojDAKBggqhkjOPQQDAgNJADBGAiEAtdZ3+05CzFo90fWeZ4woeJcNQC4B
30+
84Ill3YeZVvR8ZECIQDVRdha1xEDKuNTAManY0zthSosfXcvLnZui1A/y/DYeg==
31+
-----END CERTIFICATE-----
32+

tests/certs/es256_private.key

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgfNJBsaRLSeHizv0m
3+
GL+gcn78QmtfLSm+n+qG9veC2W2hRANCAAQPaL6RkAkYkKU4+IryBSYxJM3h77sF
4+
iMrbvbI8fG7w2Bbl9otNG/cch3DAw5rGAPV7NWkyl3QGuV/wt0MrAPDo
5+
-----END PRIVATE KEY-----

tests/certs/ps256.pem

Lines changed: 0 additions & 53 deletions
This file was deleted.

0 commit comments

Comments
 (0)