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.
- {signedshot-0.1.1 → signedshot-0.1.3}/Cargo.lock +1 -1
- {signedshot-0.1.1 → signedshot-0.1.3}/Cargo.toml +1 -1
- {signedshot-0.1.1 → signedshot-0.1.3}/PKG-INFO +1 -1
- {signedshot-0.1.1 → signedshot-0.1.3}/pyproject.toml +1 -1
- {signedshot-0.1.1 → signedshot-0.1.3}/python/signedshot/__init__.py +3 -2
- {signedshot-0.1.1 → signedshot-0.1.3}/python/signedshot/__init__.pyi +27 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/jwt.rs +8 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/python.rs +49 -1
- {signedshot-0.1.1 → signedshot-0.1.3}/src/validate.rs +43 -10
- {signedshot-0.1.1 → signedshot-0.1.3}/.github/workflows/ci.yml +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/.github/workflows/release.yml +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/.gitignore +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/README.md +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/bin/signedshot.rs +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/error.rs +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/integrity.rs +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/lib.rs +0 -0
- {signedshot-0.1.1 → signedshot-0.1.3}/src/sidecar.rs +0 -0
|
@@ -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.
|
|
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::{
|
|
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,
|
|
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(
|
|
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(
|
|
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
|
-
//
|
|
143
|
-
match
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|