signedshot 0.1.2__tar.gz → 0.1.4__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.2 → signedshot-0.1.4}/Cargo.lock +1 -1
- {signedshot-0.1.2 → signedshot-0.1.4}/Cargo.toml +1 -1
- {signedshot-0.1.2 → signedshot-0.1.4}/PKG-INFO +1 -1
- {signedshot-0.1.2 → signedshot-0.1.4}/pyproject.toml +1 -1
- {signedshot-0.1.2 → signedshot-0.1.4}/python/signedshot/__init__.py +3 -2
- {signedshot-0.1.2 → signedshot-0.1.4}/python/signedshot/__init__.pyi +27 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/src/bin/signedshot.rs +3 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/src/jwt.rs +34 -9
- {signedshot-0.1.2 → signedshot-0.1.4}/src/python.rs +1 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/src/validate.rs +6 -1
- {signedshot-0.1.2 → signedshot-0.1.4}/.github/workflows/ci.yml +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/.github/workflows/release.yml +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/.gitignore +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/README.md +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/src/error.rs +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/src/integrity.rs +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/src/lib.rs +0 -0
- {signedshot-0.1.2 → signedshot-0.1.4}/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
|
|
|
@@ -151,6 +151,9 @@ fn validate_command(sidecar_path: &Path, media_path: &Path, json_output: bool) -
|
|
|
151
151
|
println!(" Publisher ID: {}", result.capture_trust.publisher_id);
|
|
152
152
|
println!(" Device ID: {}", result.capture_trust.device_id);
|
|
153
153
|
println!(" Method: {}", result.capture_trust.method);
|
|
154
|
+
if let Some(ref app_id) = result.capture_trust.app_id {
|
|
155
|
+
println!(" App ID: {}", app_id);
|
|
156
|
+
}
|
|
154
157
|
println!(" Issued At: {}", result.capture_trust.issued_at);
|
|
155
158
|
println!(" Key ID: {}", kid);
|
|
156
159
|
|
|
@@ -27,6 +27,16 @@ pub struct JwtHeader {
|
|
|
27
27
|
pub kid: Option<String>,
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/// Attestation information from the JWT
|
|
31
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
32
|
+
pub struct Attestation {
|
|
33
|
+
/// Attestation method (sandbox, app_check, app_attest)
|
|
34
|
+
pub method: String,
|
|
35
|
+
/// App ID from attestation (e.g., bundle ID), if available
|
|
36
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
37
|
+
pub app_id: Option<String>,
|
|
38
|
+
}
|
|
39
|
+
|
|
30
40
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
31
41
|
pub struct CaptureTrustClaims {
|
|
32
42
|
pub iss: String,
|
|
@@ -36,7 +46,7 @@ pub struct CaptureTrustClaims {
|
|
|
36
46
|
pub capture_id: String,
|
|
37
47
|
pub publisher_id: String,
|
|
38
48
|
pub device_id: String,
|
|
39
|
-
pub
|
|
49
|
+
pub attestation: Attestation,
|
|
40
50
|
}
|
|
41
51
|
|
|
42
52
|
#[derive(Debug, Clone)]
|
|
@@ -97,10 +107,10 @@ fn validate_claims(claims: &CaptureTrustClaims) -> Result<()> {
|
|
|
97
107
|
}
|
|
98
108
|
|
|
99
109
|
let valid_methods = ["sandbox", "app_check", "app_attest"];
|
|
100
|
-
if !valid_methods.contains(&claims.method.as_str()) {
|
|
110
|
+
if !valid_methods.contains(&claims.attestation.method.as_str()) {
|
|
101
111
|
return Err(ValidationError::InvalidJwt(format!(
|
|
102
|
-
"Invalid method '{}', expected one of: {:?}",
|
|
103
|
-
claims.method, valid_methods
|
|
112
|
+
"Invalid attestation method '{}', expected one of: {:?}",
|
|
113
|
+
claims.attestation.method, valid_methods
|
|
104
114
|
)));
|
|
105
115
|
}
|
|
106
116
|
|
|
@@ -179,19 +189,34 @@ mod tests {
|
|
|
179
189
|
#[test]
|
|
180
190
|
fn parse_valid_jwt() {
|
|
181
191
|
let header = r#"{"alg":"ES256","typ":"JWT","kid":"test-key"}"#;
|
|
182
|
-
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","method":"sandbox"}"#;
|
|
192
|
+
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","attestation":{"method":"sandbox"}}"#;
|
|
183
193
|
let token = make_jwt(header, payload);
|
|
184
194
|
|
|
185
195
|
let parsed = parse_jwt(&token).unwrap();
|
|
186
196
|
assert_eq!(parsed.header.alg, "ES256");
|
|
187
197
|
assert_eq!(parsed.claims.capture_id, "123");
|
|
188
|
-
assert_eq!(parsed.claims.method, "sandbox");
|
|
198
|
+
assert_eq!(parsed.claims.attestation.method, "sandbox");
|
|
199
|
+
assert_eq!(parsed.claims.attestation.app_id, None);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#[test]
|
|
203
|
+
fn parse_jwt_with_app_id() {
|
|
204
|
+
let header = r#"{"alg":"ES256","typ":"JWT","kid":"test-key"}"#;
|
|
205
|
+
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","attestation":{"method":"app_check","app_id":"io.foo.bar"}}"#;
|
|
206
|
+
let token = make_jwt(header, payload);
|
|
207
|
+
|
|
208
|
+
let parsed = parse_jwt(&token).unwrap();
|
|
209
|
+
assert_eq!(parsed.claims.attestation.method, "app_check");
|
|
210
|
+
assert_eq!(
|
|
211
|
+
parsed.claims.attestation.app_id,
|
|
212
|
+
Some("io.foo.bar".to_string())
|
|
213
|
+
);
|
|
189
214
|
}
|
|
190
215
|
|
|
191
216
|
#[test]
|
|
192
217
|
fn reject_invalid_algorithm() {
|
|
193
218
|
let header = r#"{"alg":"HS256","typ":"JWT"}"#;
|
|
194
|
-
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","method":"sandbox"}"#;
|
|
219
|
+
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","attestation":{"method":"sandbox"}}"#;
|
|
195
220
|
let token = make_jwt(header, payload);
|
|
196
221
|
|
|
197
222
|
let result = parse_jwt(&token);
|
|
@@ -201,7 +226,7 @@ mod tests {
|
|
|
201
226
|
#[test]
|
|
202
227
|
fn reject_invalid_audience() {
|
|
203
228
|
let header = r#"{"alg":"ES256","typ":"JWT"}"#;
|
|
204
|
-
let payload = r#"{"iss":"https://example.com","aud":"wrong","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","method":"sandbox"}"#;
|
|
229
|
+
let payload = r#"{"iss":"https://example.com","aud":"wrong","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","attestation":{"method":"sandbox"}}"#;
|
|
205
230
|
let token = make_jwt(header, payload);
|
|
206
231
|
|
|
207
232
|
let result = parse_jwt(&token);
|
|
@@ -211,7 +236,7 @@ mod tests {
|
|
|
211
236
|
#[test]
|
|
212
237
|
fn reject_invalid_method() {
|
|
213
238
|
let header = r#"{"alg":"ES256","typ":"JWT"}"#;
|
|
214
|
-
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","method":"invalid"}"#;
|
|
239
|
+
let payload = r#"{"iss":"https://dev-api.signedshot.io","aud":"signedshot","sub":"capture-service","iat":1705312200,"capture_id":"123","publisher_id":"456","device_id":"789","attestation":{"method":"invalid"}}"#;
|
|
215
240
|
let token = make_jwt(header, payload);
|
|
216
241
|
|
|
217
242
|
let result = parse_jwt(&token);
|
|
@@ -35,6 +35,7 @@ impl PyValidationResult {
|
|
|
35
35
|
dict.set_item("device_id", &self.inner.capture_trust.device_id)?;
|
|
36
36
|
dict.set_item("capture_id", &self.inner.capture_trust.capture_id)?;
|
|
37
37
|
dict.set_item("method", &self.inner.capture_trust.method)?;
|
|
38
|
+
dict.set_item("app_id", &self.inner.capture_trust.app_id)?;
|
|
38
39
|
dict.set_item("issued_at", self.inner.capture_trust.issued_at)?;
|
|
39
40
|
dict.set_item("key_id", &self.inner.capture_trust.key_id)?;
|
|
40
41
|
Ok(dict)
|
|
@@ -29,6 +29,8 @@ pub struct CaptureTrustResult {
|
|
|
29
29
|
pub capture_id: String,
|
|
30
30
|
/// Attestation method (sandbox, app_check, app_attest)
|
|
31
31
|
pub method: String,
|
|
32
|
+
/// App ID from attestation (e.g., bundle ID), if available
|
|
33
|
+
pub app_id: Option<String>,
|
|
32
34
|
/// Unix timestamp when the JWT was issued
|
|
33
35
|
pub issued_at: i64,
|
|
34
36
|
/// Key ID used to sign the JWT
|
|
@@ -47,7 +49,8 @@ impl CaptureTrustResult {
|
|
|
47
49
|
publisher_id: claims.publisher_id.clone(),
|
|
48
50
|
device_id: claims.device_id.clone(),
|
|
49
51
|
capture_id: claims.capture_id.clone(),
|
|
50
|
-
method: claims.method.clone(),
|
|
52
|
+
method: claims.attestation.method.clone(),
|
|
53
|
+
app_id: claims.attestation.app_id.clone(),
|
|
51
54
|
issued_at: claims.iat,
|
|
52
55
|
key_id,
|
|
53
56
|
}
|
|
@@ -257,6 +260,7 @@ mod tests {
|
|
|
257
260
|
device_id: "dev-456".to_string(),
|
|
258
261
|
capture_id: "cap-789".to_string(),
|
|
259
262
|
method: "sandbox".to_string(),
|
|
263
|
+
app_id: None,
|
|
260
264
|
issued_at: 1705312200,
|
|
261
265
|
key_id: Some("key-1".to_string()),
|
|
262
266
|
},
|
|
@@ -289,6 +293,7 @@ mod tests {
|
|
|
289
293
|
device_id: "dev-456".to_string(),
|
|
290
294
|
capture_id: "cap-789".to_string(),
|
|
291
295
|
method: "sandbox".to_string(),
|
|
296
|
+
app_id: None,
|
|
292
297
|
issued_at: 1705312200,
|
|
293
298
|
key_id: None,
|
|
294
299
|
},
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|