signedshot 0.1.1__tar.gz → 0.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1331,7 +1331,7 @@ dependencies = [
1331
1331
 
1332
1332
  [[package]]
1333
1333
  name = "signedshot-validator"
1334
- version = "0.1.1"
1334
+ version = "0.1.3"
1335
1335
  dependencies = [
1336
1336
  "anyhow",
1337
1337
  "base64",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "signedshot-validator"
3
- version = "0.1.1"
3
+ version = "0.1.3"
4
4
  edition = "2021"
5
5
  description = "Validator for SignedShot media authenticity proofs"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: signedshot
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Classifier: Development Status :: 4 - Beta
5
5
  Classifier: Intended Audience :: Developers
6
6
  Classifier: License :: OSI Approved :: MIT License
@@ -4,7 +4,7 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "signedshot"
7
- version = "0.1.1"
7
+ version = "0.1.3"
8
8
  description = "Validator for SignedShot media authenticity proofs"
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -3,7 +3,8 @@ from signedshot.signedshot import (
3
3
  ValidationResult,
4
4
  validate,
5
5
  validate_files,
6
+ validate_with_jwks,
6
7
  )
7
8
 
8
- __all__ = ["ValidationResult", "validate", "validate_files"]
9
- __version__ = "0.1.0"
9
+ __all__ = ["ValidationResult", "validate", "validate_files", "validate_with_jwks"]
10
+ __version__ = "0.1.3"
@@ -82,6 +82,33 @@ def validate(sidecar_json: str, media_bytes: bytes) -> ValidationResult:
82
82
  """
83
83
  ...
84
84
 
85
+ def validate_with_jwks(
86
+ sidecar_json: str, media_bytes: bytes, jwks_json: str
87
+ ) -> ValidationResult:
88
+ """Validate a SignedShot sidecar against media content using pre-loaded JWKS.
89
+
90
+ Use this when you already have the JWKS available locally, avoiding HTTP fetch.
91
+ This is useful for API services that want to validate using their own keys.
92
+
93
+ Args:
94
+ sidecar_json: The sidecar JSON as a string
95
+ media_bytes: The media file content as bytes
96
+ jwks_json: The JWKS JSON as a string (from /.well-known/jwks.json)
97
+
98
+ Returns:
99
+ ValidationResult with detailed information about the validation
100
+
101
+ Raises:
102
+ ValueError: If the sidecar or JWKS cannot be parsed
103
+
104
+ Example:
105
+ >>> jwks_json = '{"keys": [...]}'
106
+ >>> result = validate_with_jwks(sidecar_json, media_bytes, jwks_json)
107
+ >>> if result.valid:
108
+ ... print(f"Publisher: {result.capture_trust['publisher_id']}")
109
+ """
110
+ ...
111
+
85
112
  def validate_files(sidecar_path: str, media_path: str) -> ValidationResult:
86
113
  """Validate a SignedShot sidecar from file paths.
87
114
 
@@ -126,6 +126,14 @@ pub fn fetch_jwks(issuer: &str) -> Result<Jwks> {
126
126
  .map_err(|e| ValidationError::JwksFetchError(format!("Failed to parse JWKS: {}", e)))
127
127
  }
128
128
 
129
+ /// Parse JWKS from a JSON string.
130
+ ///
131
+ /// Useful when the JWKS is already available locally (e.g., from the API's own keys).
132
+ pub fn parse_jwks_json(jwks_json: &str) -> Result<Jwks> {
133
+ serde_json::from_str(jwks_json)
134
+ .map_err(|e| ValidationError::JwksFetchError(format!("Failed to parse JWKS JSON: {}", e)))
135
+ }
136
+
129
137
  pub fn verify_signature(token: &str, jwks: &Jwks, kid: &str) -> Result<()> {
130
138
  let jwk = jwks
131
139
  .keys
@@ -6,7 +6,9 @@
6
6
  use pyo3::prelude::*;
7
7
  use pyo3::types::PyDict;
8
8
 
9
- use crate::validate::{validate_from_bytes, ValidationResult as RustValidationResult};
9
+ use crate::validate::{
10
+ validate_from_bytes, validate_from_bytes_with_jwks, ValidationResult as RustValidationResult,
11
+ };
10
12
 
11
13
  /// Python-accessible validation result
12
14
  #[pyclass(name = "ValidationResult")]
@@ -150,6 +152,51 @@ fn validate(sidecar_json: &str, media_bytes: &[u8]) -> PyResult<PyValidationResu
150
152
  .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))
151
153
  }
152
154
 
155
+ /// Validate a SignedShot sidecar against media content using pre-loaded JWKS.
156
+ ///
157
+ /// Use this when you already have the JWKS available locally, avoiding HTTP fetch.
158
+ /// This is useful for API services that want to validate using their own keys.
159
+ ///
160
+ /// Args:
161
+ /// sidecar_json: The sidecar JSON as a string
162
+ /// media_bytes: The media file content as bytes
163
+ /// jwks_json: The JWKS JSON as a string (from /.well-known/jwks.json)
164
+ ///
165
+ /// Returns:
166
+ /// ValidationResult: The validation result with detailed information
167
+ ///
168
+ /// Raises:
169
+ /// ValueError: If the sidecar or JWKS cannot be parsed
170
+ ///
171
+ /// Example:
172
+ /// ```python
173
+ /// import signedshot
174
+ ///
175
+ /// # Get JWKS from your service's keys
176
+ /// jwks_json = '{"keys": [...]}'
177
+ ///
178
+ /// with open("photo.sidecar.json") as f:
179
+ /// sidecar_json = f.read()
180
+ /// with open("photo.jpg", "rb") as f:
181
+ /// media_bytes = f.read()
182
+ ///
183
+ /// result = signedshot.validate_with_jwks(sidecar_json, media_bytes, jwks_json)
184
+ /// if result.valid:
185
+ /// print(f"Valid! Publisher: {result.capture_trust['publisher_id']}")
186
+ /// else:
187
+ /// print(f"Invalid: {result.error}")
188
+ /// ```
189
+ #[pyfunction]
190
+ fn validate_with_jwks(
191
+ sidecar_json: &str,
192
+ media_bytes: &[u8],
193
+ jwks_json: &str,
194
+ ) -> PyResult<PyValidationResult> {
195
+ validate_from_bytes_with_jwks(sidecar_json, media_bytes, jwks_json)
196
+ .map(PyValidationResult::from)
197
+ .map_err(|e| PyErr::new::<pyo3::exceptions::PyValueError, _>(e.to_string()))
198
+ }
199
+
153
200
  /// Validate a SignedShot sidecar from file paths.
154
201
  ///
155
202
  /// Args:
@@ -220,6 +267,7 @@ fn validate_files(sidecar_path: &str, media_path: &str) -> PyResult<PyValidation
220
267
  fn signedshot(m: &Bound<'_, PyModule>) -> PyResult<()> {
221
268
  m.add_class::<PyValidationResult>()?;
222
269
  m.add_function(wrap_pyfunction!(validate, m)?)?;
270
+ m.add_function(wrap_pyfunction!(validate_with_jwks, m)?)?;
223
271
  m.add_function(wrap_pyfunction!(validate_files, m)?)?;
224
272
  Ok(())
225
273
  }
@@ -9,7 +9,8 @@ use std::path::Path;
9
9
  use crate::error::{Result, ValidationError};
10
10
  use crate::integrity::{verify_capture_id_match, verify_signature as verify_media_signature};
11
11
  use crate::jwt::{
12
- fetch_jwks, parse_jwt, verify_signature as verify_jwt_signature, CaptureTrustClaims,
12
+ fetch_jwks, parse_jwks_json, parse_jwt, verify_signature as verify_jwt_signature,
13
+ CaptureTrustClaims, Jwks,
13
14
  };
14
15
  use crate::sidecar::Sidecar;
15
16
 
@@ -105,7 +106,20 @@ pub fn validate(sidecar_path: &Path, media_path: &Path) -> Result<ValidationResu
105
106
  ///
106
107
  /// Useful when you have the content in memory rather than files.
107
108
  pub fn validate_from_bytes(sidecar_json: &str, media_bytes: &[u8]) -> Result<ValidationResult> {
108
- validate_bytes_impl(sidecar_json, media_bytes)
109
+ validate_bytes_impl(sidecar_json, media_bytes, None)
110
+ }
111
+
112
+ /// Validate from sidecar JSON string and media bytes with pre-loaded JWKS.
113
+ ///
114
+ /// Use this when you already have the JWKS available locally, avoiding HTTP fetch.
115
+ /// This is useful for the API service that wants to validate using its own keys.
116
+ pub fn validate_from_bytes_with_jwks(
117
+ sidecar_json: &str,
118
+ media_bytes: &[u8],
119
+ jwks_json: &str,
120
+ ) -> Result<ValidationResult> {
121
+ let jwks = parse_jwks_json(jwks_json)?;
122
+ validate_bytes_impl(sidecar_json, media_bytes, Some(jwks))
109
123
  }
110
124
 
111
125
  fn validate_impl(sidecar_path: &Path, media_path: &Path) -> Result<ValidationResult> {
@@ -115,17 +129,25 @@ fn validate_impl(sidecar_path: &Path, media_path: &Path) -> Result<ValidationRes
115
129
  // Read media file for hash verification
116
130
  let media_bytes = std::fs::read(media_path)?;
117
131
 
118
- validate_sidecar_and_media(&sidecar, &media_bytes)
132
+ validate_sidecar_and_media(&sidecar, &media_bytes, None)
119
133
  }
120
134
 
121
- fn validate_bytes_impl(sidecar_json: &str, media_bytes: &[u8]) -> Result<ValidationResult> {
135
+ fn validate_bytes_impl(
136
+ sidecar_json: &str,
137
+ media_bytes: &[u8],
138
+ jwks: Option<Jwks>,
139
+ ) -> Result<ValidationResult> {
122
140
  // Parse sidecar
123
141
  let sidecar = Sidecar::from_json(sidecar_json)?;
124
142
 
125
- validate_sidecar_and_media(&sidecar, media_bytes)
143
+ validate_sidecar_and_media(&sidecar, media_bytes, jwks)
126
144
  }
127
145
 
128
- fn validate_sidecar_and_media(sidecar: &Sidecar, media_bytes: &[u8]) -> Result<ValidationResult> {
146
+ fn validate_sidecar_and_media(
147
+ sidecar: &Sidecar,
148
+ media_bytes: &[u8],
149
+ jwks: Option<Jwks>,
150
+ ) -> Result<ValidationResult> {
129
151
  let integrity = sidecar.media_integrity();
130
152
 
131
153
  // Parse JWT (without signature verification yet)
@@ -139,8 +161,8 @@ fn validate_sidecar_and_media(sidecar: &Sidecar, media_bytes: &[u8]) -> Result<V
139
161
  let mut capture_id_match = false;
140
162
  let mut error_message: Option<String> = None;
141
163
 
142
- // Fetch JWKS and verify JWT signature
143
- match fetch_and_verify_jwt(sidecar.jwt(), &parsed.claims.iss, kid.as_deref()) {
164
+ // Verify JWT signature (using provided JWKS or fetching from issuer)
165
+ match verify_jwt_with_jwks(sidecar.jwt(), &parsed.claims.iss, kid.as_deref(), jwks) {
144
166
  Ok(()) => jwt_signature_valid = true,
145
167
  Err(e) => {
146
168
  error_message = Some(format!("JWT verification failed: {}", e));
@@ -198,11 +220,22 @@ fn validate_sidecar_and_media(sidecar: &Sidecar, media_bytes: &[u8]) -> Result<V
198
220
  })
199
221
  }
200
222
 
201
- fn fetch_and_verify_jwt(token: &str, issuer: &str, kid: Option<&str>) -> Result<()> {
223
+ /// Verify JWT signature using provided JWKS or by fetching from issuer.
224
+ fn verify_jwt_with_jwks(
225
+ token: &str,
226
+ issuer: &str,
227
+ kid: Option<&str>,
228
+ jwks: Option<Jwks>,
229
+ ) -> Result<()> {
202
230
  let kid =
203
231
  kid.ok_or_else(|| ValidationError::InvalidJwt("JWT missing kid in header".to_string()))?;
204
232
 
205
- let jwks = fetch_jwks(issuer)?;
233
+ // Use provided JWKS or fetch from issuer
234
+ let jwks = match jwks {
235
+ Some(jwks) => jwks,
236
+ None => fetch_jwks(issuer)?,
237
+ };
238
+
206
239
  verify_jwt_signature(token, &jwks, kid)?;
207
240
 
208
241
  Ok(())
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes