Skip to content

Commit ec28b65

Browse files
authored
Merge pull request #34 from contentauth/gpeacock/sign_file_peformance
Performance improvement when signing using file paths.
2 parents 89a0110 + e815143 commit ec28b65

File tree

10 files changed

+114
-41
lines changed

10 files changed

+114
-41
lines changed

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ crate-type = ["cdylib"]
1111

1212

1313
[dependencies]
14-
c2pa = {version = "0.35.0", features = ["unstable_api", "openssl", "pdf", "fetch_remote_manifests"]}
14+
c2pa = {version = "0.35.0", features = ["unstable_api", "file_io", "openssl", "pdf", "fetch_remote_manifests"]}
1515
pem = "3.0.3"
1616
serde = { version = "1.0.197", features = ["derive"] }
1717
serde_derive = "1.0"

c2pa/c2pa_api/c2pa_api.py

Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -126,30 +126,11 @@ def from_archive(cls, stream):
126126
return self
127127

128128
def sign(self, signer, format, input, output = None):
129-
return super().sign(format, C2paStream(input), C2paStream(output), signer)
129+
return super().sign(signer, format, C2paStream(input), C2paStream(output))
130130

131131
def sign_file(self, signer, sourcePath, outputPath):
132-
extension = os.path.splitext(outputPath)[1][1:]
133-
input = open(sourcePath, "rb")
134-
# Check if outputPath is the same as sourcePath
135-
if outputPath == sourcePath:
136-
# Create a temporary file with the same extension as sourcePath
137-
temp_output = tempfile.NamedTemporaryFile(suffix='.' + extension, delete=False)
138-
temp_output_path = temp_output.name
139-
temp_output.close()
140-
else:
141-
temp_output_path = outputPath
142-
143-
output = open(temp_output_path, "wb")
144-
result = self.sign(signer, extension, input, output)
145-
output.close()
146-
input.close()
147-
148-
# If a temporary file was used, rename or copy it to the outputPath
149-
if outputPath == sourcePath:
150-
shutil.move(temp_output_path, outputPath)
151-
152-
return result
132+
return super().sign_file(signer, sourcePath, outputPath)
133+
153134

154135

155136
# Implements a C2paStream given a stream handle
@@ -225,12 +206,12 @@ def sign_ps256_shell(data: bytes, key_path: str) -> bytes:
225206
from cryptography.hazmat.primitives import hashes, serialization
226207
from cryptography.hazmat.primitives.asymmetric import padding
227208

228-
def sign_ps256(data: bytes, key_path: str) -> bytes:
229-
with open(key_path, "rb") as key_file:
230-
private_key = serialization.load_pem_private_key(
231-
key_file.read(),
232-
password=None,
233-
)
209+
def sign_ps256(data: bytes, key: bytes) -> bytes:
210+
211+
private_key = serialization.load_pem_private_key(
212+
key,
213+
password=None,
214+
)
234215
signature = private_key.sign(
235216
data,
236217
padding.PSS(

src/c2pa.udl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,8 @@ interface Builder {
9292
void from_archive([ByRef] Stream stream );
9393

9494
[Throws=Error]
95-
bytes sign([ByRef] string format, [ByRef] Stream input, [ByRef] Stream output ,[ByRef] CallbackSigner signer);
95+
bytes sign([ByRef] CallbackSigner signer, [ByRef] string format, [ByRef] Stream input, [ByRef] Stream output);
96+
97+
[Throws=Error]
98+
bytes sign_file([ByRef] CallbackSigner signer, [ByRef] string input, [ByRef] string output);
9699
};

src/callback_signer.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ impl CallbackSigner {
3434
certs: Vec<u8>,
3535
ta_url: Option<String>,
3636
) -> Self {
37-
3837
// When this closure is called it will call the sign method on the python callback
3938
let python_signer = move |_context: *const (), data: &[u8]| {
4039
callback

src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,10 +172,10 @@ impl Builder {
172172
/// Sign an asset and write the result to the destination stream
173173
pub fn sign(
174174
&self,
175+
signer: &CallbackSigner,
175176
format: &str,
176177
source: &dyn Stream,
177178
dest: &dyn Stream,
178-
signer: &CallbackSigner,
179179
) -> Result<Vec<u8>> {
180180
// uniffi doesn't allow mutable parameters, so we we use an adapter
181181
let mut source = StreamAdapter::from(source);
@@ -187,4 +187,14 @@ impl Builder {
187187
Err(Error::RwLock)
188188
}
189189
}
190+
191+
/// Sign an asset and write the result to the destination stream
192+
pub fn sign_file(&self, signer: &CallbackSigner, source: &str, dest: &str) -> Result<Vec<u8>> {
193+
if let Ok(mut builder) = self.builder.try_write() {
194+
let signer = (*signer).signer();
195+
Ok(builder.sign_file(signer, source, dest)?)
196+
} else {
197+
Err(Error::RwLock)
198+
}
199+
}
190200
}

src/streams.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,12 @@ impl<'a> From<&'a dyn Stream> for StreamAdapter<'a> {
8282

8383
impl<'a> Read for StreamAdapter<'a> {
8484
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
85-
let mut bytes = self
85+
let bytes = self
8686
.stream
8787
.read_stream(buf.len() as u64)
8888
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
8989
let len = bytes.len();
90-
buf.iter_mut().zip(bytes.drain(..)).for_each(|(dest, src)| {
91-
*dest = src;
92-
});
90+
buf[..len].copy_from_slice(&bytes);
9391
//println!("read: {:?}", len);
9492
Ok(len)
9593
}

tests/benchmark.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from c2pa import Builder, Error, Reader, SigningAlg, create_signer, sdk_version, sign_ps256
2+
import os
3+
import io
4+
PROJECT_PATH = os.getcwd()
5+
6+
testPath = os.path.join(PROJECT_PATH, "tests", "fixtures", "C.jpg")
7+
8+
manifestDefinition = {
9+
"claim_generator": "python_test",
10+
"claim_generator_info": [{
11+
"name": "python_test",
12+
"version": "0.0.1",
13+
}],
14+
"format": "image/jpeg",
15+
"title": "Python Test Image",
16+
"ingredients": [],
17+
"assertions": [
18+
{ 'label': 'stds.schema-org.CreativeWork',
19+
'data': {
20+
'@context': 'http://schema.org/',
21+
'@type': 'CreativeWork',
22+
'author': [
23+
{ '@type': 'Person',
24+
'name': 'Gavin Peacock'
25+
}
26+
]
27+
},
28+
'kind': 'Json'
29+
}
30+
]
31+
}
32+
private_key = open("tests/fixtures/ps256.pem","rb").read()
33+
34+
# Define a function that signs data with PS256 using a private key
35+
def sign(data: bytes) -> bytes:
36+
print("date len = ", len(data))
37+
return sign_ps256(data, private_key)
38+
39+
# load the public keys from a pem file
40+
certs = open("tests/fixtures/ps256.pub","rb").read()
41+
42+
# Create a local Ps256 signer with certs and a timestamp server
43+
signer = create_signer(sign, SigningAlg.PS256, certs, "http://timestamp.digicert.com")
44+
45+
builder = Builder(manifestDefinition)
46+
47+
source = open(testPath, "rb").read()
48+
49+
testPath = "/Users/gpeacock/Pictures/Lightroom Saved Photos/IMG_0483.jpg"
50+
testPath = "tests/fixtures/c.jpg"
51+
outputPath = "target/python_out.jpg"
52+
53+
def test_files_build():
54+
# Delete the output file if it exists
55+
if os.path.exists(outputPath):
56+
os.remove(outputPath)
57+
builder.sign_file(signer, testPath, outputPath)
58+
59+
def test_streams_build():
60+
#with open(testPath, "rb") as file:
61+
output = io.BytesIO(bytearray())
62+
builder.sign(signer, "image/jpeg", io.BytesIO(source), output)
63+
64+
def test_func(benchmark):
65+
benchmark(test_files_build)
66+
67+
def test_streams(benchmark):
68+
benchmark(test_streams_build)
69+
70+
#def test_signer(benchmark):
71+
# benchmark(sign_ps256, data, private_key)

tests/test_api.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
# each license.
1212

1313
import json
14+
import os
1415
import pytest
1516
import tempfile
1617
import shutil
@@ -98,8 +99,9 @@ def test_v2_sign():
9899
# define a source folder for any assets we need to read
99100
data_dir = "tests/fixtures/"
100101
try:
102+
key = open(data_dir + "ps256.pem", "rb").read()
101103
def sign(data: bytes) -> bytes:
102-
return sign_ps256(data, data_dir+"ps256.pem")
104+
return sign_ps256(data, key)
103105

104106
certs = open(data_dir + "ps256.pub", "rb").read()
105107
# Create a local signer from a certificate pem file
@@ -114,6 +116,9 @@ def sign(data: bytes) -> bytes:
114116
builder = Builder.from_archive(open("target/archive.zip", "rb"))
115117

116118
with tempfile.TemporaryDirectory() as output_dir:
119+
output_path = output_dir + "out.jpg"
120+
if os.path.exists(output_path):
121+
os.remove(output_path)
117122
c2pa_data = builder.sign_file(signer, data_dir + "A.jpg", output_dir + "out.jpg")
118123
assert len(c2pa_data) > 0
119124

@@ -135,8 +140,9 @@ def sign(data: bytes) -> bytes:
135140
def test_v2_sign_file_same():
136141
data_dir = "tests/fixtures/"
137142
try:
143+
key = open(data_dir + "ps256.pem", "rb").read()
138144
def sign(data: bytes) -> bytes:
139-
return sign_ps256(data, data_dir+"ps256.pem")
145+
return sign_ps256(data, key)
140146

141147
certs = open(data_dir + "ps256.pub", "rb").read()
142148
# Create a local signer from a certificate pem file
@@ -163,4 +169,4 @@ def sign(data: bytes) -> bytes:
163169
assert manifest.get("validation_status") == None
164170
except Exception as e:
165171
print("Failed to sign manifest store: " + str(e))
166-
exit(1)
172+
#exit(1)

tests/training.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,9 @@ def getitem(d, key):
7373
# V2 signing api
7474
try:
7575
# This could be implemented on a server using an HSM
76+
key = open("tests/fixtures/ps256.pem","rb").read()
7677
def sign(data: bytes) -> bytes:
77-
return sign_ps256(data, "tests/fixtures/ps256.pem")
78+
return sign_ps256(data, key)
7879

7980
certs = open("tests/fixtures/ps256.pub","rb").read()
8081

@@ -87,6 +88,9 @@ def sign(data: bytes) -> bytes:
8788

8889
builder.add_ingredient_file(ingredient_json, "tests/fixtures/A.jpg")
8990

91+
if os.path.exists(testOutputFile):
92+
os.remove(testOutputFile)
93+
9094
result = builder.sign_file(signer, testFile, testOutputFile)
9195

9296
except Exception as err:

tests/unit_tests.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ class TestBuilder(unittest.TestCase):
8383

8484
# Define a function that signs data with PS256 using a private key
8585
def sign(data: bytes) -> bytes:
86-
return sign_ps256(data, "tests/fixtures/ps256.pem")
86+
key = open("tests/fixtures/ps256.pem","rb").read()
87+
return sign_ps256(data, key)
8788

8889
# load the public keys from a pem file
8990
certs = open("tests/fixtures/ps256.pub","rb").read()

0 commit comments

Comments
 (0)