moss-sdk 0.1.0__py3-none-any.whl

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.
moss/__init__.py ADDED
@@ -0,0 +1,28 @@
1
+ from .subject import Subject, VerifyResult
2
+ from .envelope import Envelope
3
+ from .errors import (
4
+ MossError,
5
+ InvalidSubject,
6
+ KeyNotFound,
7
+ InvalidEnvelope,
8
+ InvalidSignature,
9
+ PayloadMismatch,
10
+ ReplayDetected,
11
+ DecryptionFailed,
12
+ )
13
+
14
+ __version__ = "0.1.0"
15
+
16
+ __all__ = [
17
+ "Subject",
18
+ "Envelope",
19
+ "VerifyResult",
20
+ "MossError",
21
+ "InvalidSubject",
22
+ "KeyNotFound",
23
+ "InvalidEnvelope",
24
+ "InvalidSignature",
25
+ "PayloadMismatch",
26
+ "ReplayDetected",
27
+ "DecryptionFailed",
28
+ ]
moss/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
moss/canonical.py ADDED
@@ -0,0 +1,29 @@
1
+ import json
2
+ import hashlib
3
+
4
+ from .encoding import b64url_encode
5
+
6
+
7
+ def cjjson(obj: dict) -> bytes:
8
+ """
9
+ RFC 8785 JSON Canonicalization Scheme.
10
+ - Sort keys lexicographically (Unicode code point order)
11
+ - No whitespace
12
+ - UTF-8 encoding
13
+ """
14
+ return json.dumps(
15
+ obj,
16
+ separators=(',', ':'),
17
+ sort_keys=True,
18
+ ensure_ascii=False
19
+ ).encode('utf-8')
20
+
21
+
22
+ def sha256_b64url(data: bytes) -> str:
23
+ """SHA-256 hash, returned as base64url (43 chars)."""
24
+ return b64url_encode(hashlib.sha256(data).digest())
25
+
26
+
27
+ def payload_hash(payload: dict) -> str:
28
+ """Compute payload_hash per moss-0001."""
29
+ return sha256_b64url(cjjson(payload))
moss/cli.py ADDED
@@ -0,0 +1,109 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ from . import Subject, Envelope
5
+
6
+
7
+ def main():
8
+ parser = argparse.ArgumentParser(prog="moss", description="MOSS CLI - Sign and verify agent outputs")
9
+ subparsers = parser.add_subparsers(dest="command")
10
+
11
+ # moss subject create <subject>
12
+ sub_parser = subparsers.add_parser("subject", help="Manage subjects")
13
+ sub_sub = sub_parser.add_subparsers(dest="subcommand")
14
+ create_parser = sub_sub.add_parser("create", help="Create new subject")
15
+ create_parser.add_argument("subject", help="Subject ID (moss:namespace:name)")
16
+
17
+ # moss sign <subject> <payload_file>
18
+ sign_parser = subparsers.add_parser("sign", help="Sign a payload")
19
+ sign_parser.add_argument("subject", help="Subject to sign as")
20
+ sign_parser.add_argument("payload_file", help="JSON payload file (- for stdin)")
21
+
22
+ # moss verify <payload_file> <envelope_file>
23
+ verify_parser = subparsers.add_parser("verify", help="Verify an envelope")
24
+ verify_parser.add_argument("payload_file", help="JSON payload file")
25
+ verify_parser.add_argument("envelope_file", help="Envelope JSON file")
26
+ verify_parser.add_argument("--no-replay-check", action="store_true", help="Skip replay detection")
27
+
28
+ # moss diff <env1> <env2>
29
+ diff_parser = subparsers.add_parser("diff", help="Compare two envelopes")
30
+ diff_parser.add_argument("envelope1", help="First envelope file")
31
+ diff_parser.add_argument("envelope2", help="Second envelope file")
32
+
33
+ args = parser.parse_args()
34
+
35
+ try:
36
+ if args.command == "subject" and args.subcommand == "create":
37
+ subject = Subject.create(args.subject)
38
+ print(f"Created: {args.subject}")
39
+ print(f" Public key: {subject.public_key.hex()[:32]}...")
40
+ sys.exit(0)
41
+
42
+ elif args.command == "sign":
43
+ subject = Subject.load(args.subject)
44
+ if args.payload_file == "-":
45
+ payload = json.load(sys.stdin)
46
+ else:
47
+ with open(args.payload_file) as f:
48
+ payload = json.load(f)
49
+ envelope = subject.sign(payload)
50
+ print(json.dumps(envelope.to_dict(), indent=2))
51
+ sys.exit(0)
52
+
53
+ elif args.command == "verify":
54
+ with open(args.payload_file) as f:
55
+ payload = json.load(f)
56
+ with open(args.envelope_file) as f:
57
+ envelope_data = json.load(f)
58
+ envelope = Envelope.from_dict(envelope_data)
59
+ result = Subject.verify(
60
+ envelope,
61
+ payload,
62
+ check_replay=not args.no_replay_check
63
+ )
64
+ if result.valid:
65
+ print(f"Valid signature from {result.subject}")
66
+ sys.exit(0)
67
+ else:
68
+ print(f"Invalid: {result.reason} [{result.error_code}]")
69
+ sys.exit(1)
70
+
71
+ elif args.command == "diff":
72
+ with open(args.envelope1) as f:
73
+ env1 = json.load(f)
74
+ with open(args.envelope2) as f:
75
+ env2 = json.load(f)
76
+
77
+ diffs = []
78
+ all_keys = set(env1.keys()) | set(env2.keys())
79
+ for key in sorted(all_keys):
80
+ v1 = env1.get(key)
81
+ v2 = env2.get(key)
82
+ if v1 != v2:
83
+ diffs.append(f" {key}: {v1} -> {v2}")
84
+
85
+ if diffs:
86
+ print("Differences:")
87
+ print("\n".join(diffs))
88
+ else:
89
+ print("Envelopes are identical")
90
+ sys.exit(0)
91
+
92
+ else:
93
+ parser.print_help()
94
+ sys.exit(3)
95
+
96
+ except FileNotFoundError as e:
97
+ print(f"File not found: {e}", file=sys.stderr)
98
+ sys.exit(2)
99
+ except json.JSONDecodeError as e:
100
+ print(f"Invalid JSON: {e}", file=sys.stderr)
101
+ sys.exit(2)
102
+ except Exception as e:
103
+ code = getattr(e, 'code', 'MOSS_ERR_000')
104
+ print(f"{e} [{code}]", file=sys.stderr)
105
+ sys.exit(1)
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
moss/crypto.py ADDED
@@ -0,0 +1,29 @@
1
+ from dilithium_py.dilithium import Dilithium2
2
+
3
+
4
+ class MossCrypto:
5
+ ALG = "ML-DSA-44"
6
+ EXPECTED_PK_LEN = 1312
7
+ EXPECTED_SIG_LEN = 2420
8
+
9
+ @classmethod
10
+ def keygen(cls) -> tuple[bytes, bytes]:
11
+ """Generate (public_key, secret_key) pair."""
12
+ pk, sk = Dilithium2.keygen()
13
+ assert len(pk) == cls.EXPECTED_PK_LEN, f"Bad pk length: {len(pk)}"
14
+ return pk, sk
15
+
16
+ @classmethod
17
+ def sign(cls, secret_key: bytes, message: bytes) -> bytes:
18
+ """Sign message with secret key."""
19
+ sig = Dilithium2.sign(secret_key, message)
20
+ assert len(sig) == cls.EXPECTED_SIG_LEN, f"Bad sig length: {len(sig)}"
21
+ return sig
22
+
23
+ @classmethod
24
+ def verify(cls, public_key: bytes, message: bytes, signature: bytes) -> bool:
25
+ """Verify signature. Returns True/False, never raises."""
26
+ try:
27
+ return Dilithium2.verify(public_key, message, signature)
28
+ except Exception:
29
+ return False
moss/encoding.py ADDED
@@ -0,0 +1,14 @@
1
+ import base64
2
+
3
+
4
+ def b64url_encode(data: bytes) -> str:
5
+ """base64url encode without padding."""
6
+ return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')
7
+
8
+
9
+ def b64url_decode(s: str) -> bytes:
10
+ """base64url decode, handle missing padding."""
11
+ padding = 4 - (len(s) % 4)
12
+ if padding != 4:
13
+ s += '=' * padding
14
+ return base64.urlsafe_b64decode(s)
moss/envelope.py ADDED
@@ -0,0 +1,41 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class Envelope:
6
+ spec: str
7
+ version: int
8
+ alg: str
9
+ subject: str
10
+ key_version: int
11
+ seq: int
12
+ issued_at: int
13
+ payload_hash: str
14
+ signature: str
15
+
16
+ def to_dict(self) -> dict:
17
+ return {
18
+ "spec": self.spec,
19
+ "version": self.version,
20
+ "alg": self.alg,
21
+ "subject": self.subject,
22
+ "key_version": self.key_version,
23
+ "seq": self.seq,
24
+ "issued_at": self.issued_at,
25
+ "payload_hash": self.payload_hash,
26
+ "signature": self.signature
27
+ }
28
+
29
+ @classmethod
30
+ def from_dict(cls, d: dict) -> "Envelope":
31
+ return cls(
32
+ spec=d["spec"],
33
+ version=d["version"],
34
+ alg=d["alg"],
35
+ subject=d["subject"],
36
+ key_version=d["key_version"],
37
+ seq=d["seq"],
38
+ issued_at=d["issued_at"],
39
+ payload_hash=d["payload_hash"],
40
+ signature=d["signature"]
41
+ )
moss/errors.py ADDED
@@ -0,0 +1,38 @@
1
+ class MossError(Exception):
2
+ """Base exception for MOSS errors."""
3
+ code = "MOSS_ERR_000"
4
+
5
+
6
+ class InvalidSubject(MossError):
7
+ """Invalid subject format."""
8
+ code = "MOSS_ERR_001"
9
+
10
+
11
+ class KeyNotFound(MossError):
12
+ """No keys found for subject."""
13
+ code = "MOSS_ERR_002"
14
+
15
+
16
+ class InvalidEnvelope(MossError):
17
+ """Malformed envelope."""
18
+ code = "MOSS_ERR_003"
19
+
20
+
21
+ class InvalidSignature(MossError):
22
+ """Signature verification failed."""
23
+ code = "MOSS_ERR_004"
24
+
25
+
26
+ class PayloadMismatch(MossError):
27
+ """Payload hash does not match envelope."""
28
+ code = "MOSS_ERR_005"
29
+
30
+
31
+ class ReplayDetected(MossError):
32
+ """Sequence number already seen (replay attack)."""
33
+ code = "MOSS_ERR_006"
34
+
35
+
36
+ class DecryptionFailed(MossError):
37
+ """Failed to decrypt keystore (wrong passphrase?)."""
38
+ code = "MOSS_ERR_007"
moss/keystore.py ADDED
@@ -0,0 +1,117 @@
1
+ import os
2
+ import json
3
+ import time
4
+ from pathlib import Path
5
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
6
+ from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
7
+
8
+ from .encoding import b64url_encode, b64url_decode
9
+ from .errors import KeyNotFound, DecryptionFailed, InvalidSubject
10
+
11
+ MOSS_DIR = Path.home() / ".moss"
12
+ KEYS_DIR = MOSS_DIR / "keys"
13
+
14
+
15
+ def _get_passphrase() -> bytes:
16
+ """Get passphrase from env or use default (for dev)."""
17
+ pp = os.environ.get("MOSS_KEY_PASSPHRASE", "moss-dev-passphrase")
18
+ return pp.encode('utf-8')
19
+
20
+
21
+ def _derive_key(salt: bytes) -> bytes:
22
+ """Derive 256-bit key from passphrase using Scrypt."""
23
+ kdf = Scrypt(salt=salt, length=32, n=2**14, r=8, p=1)
24
+ return kdf.derive(_get_passphrase())
25
+
26
+
27
+ def _encrypt(data: bytes, salt: bytes) -> tuple[bytes, bytes]:
28
+ """Encrypt data with AES-256-GCM. Returns (nonce, ciphertext)."""
29
+ key = _derive_key(salt)
30
+ nonce = os.urandom(12)
31
+ aesgcm = AESGCM(key)
32
+ ct = aesgcm.encrypt(nonce, data, None)
33
+ return nonce, ct
34
+
35
+
36
+ def _decrypt(nonce: bytes, ciphertext: bytes, salt: bytes) -> bytes:
37
+ """Decrypt data with AES-256-GCM."""
38
+ key = _derive_key(salt)
39
+ aesgcm = AESGCM(key)
40
+ try:
41
+ return aesgcm.decrypt(nonce, ciphertext, None)
42
+ except Exception as e:
43
+ raise DecryptionFailed(f"Failed to decrypt key: {e}")
44
+
45
+
46
+ def get_key_path(subject: str) -> Path:
47
+ """Convert subject to file path: moss:ns:name -> ns/name.json"""
48
+ parts = subject.split(":")
49
+ if len(parts) != 3 or parts[0] != "moss":
50
+ raise InvalidSubject(f"Invalid subject format: {subject}")
51
+ namespace, name = parts[1], parts[2]
52
+ return KEYS_DIR / namespace / f"{name}.json"
53
+
54
+
55
+ def save_keys(subject: str, public_key: bytes, secret_key: bytes, key_version: int = 1):
56
+ """Save encrypted key pair to local storage."""
57
+ path = get_key_path(subject)
58
+ path.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ salt = os.urandom(16)
61
+ nonce, encrypted_sk = _encrypt(secret_key, salt)
62
+
63
+ data = {
64
+ "version": 1,
65
+ "subject": subject,
66
+ "key_version": key_version,
67
+ "alg": "ML-DSA-44",
68
+ "public_key": b64url_encode(public_key),
69
+ "encrypted_secret_key": b64url_encode(encrypted_sk),
70
+ "salt": b64url_encode(salt),
71
+ "nonce": b64url_encode(nonce),
72
+ "seq": 0,
73
+ "created_at": int(time.time())
74
+ }
75
+
76
+ path.write_text(json.dumps(data, indent=2))
77
+ os.chmod(path, 0o600)
78
+
79
+
80
+ def load_keys(subject: str) -> dict:
81
+ """Load and decrypt key data from local storage."""
82
+ path = get_key_path(subject)
83
+ if not path.exists():
84
+ raise KeyNotFound(f"No keys found for {subject}")
85
+
86
+ data = json.loads(path.read_text())
87
+
88
+ salt = b64url_decode(data["salt"])
89
+ nonce = b64url_decode(data["nonce"])
90
+ encrypted_sk = b64url_decode(data["encrypted_secret_key"])
91
+ secret_key = _decrypt(nonce, encrypted_sk, salt)
92
+
93
+ return {
94
+ "subject": data["subject"],
95
+ "key_version": data["key_version"],
96
+ "public_key": b64url_decode(data["public_key"]),
97
+ "secret_key": secret_key,
98
+ "seq": data["seq"]
99
+ }
100
+
101
+
102
+ def increment_seq(subject: str) -> int:
103
+ """Atomically increment and return next sequence number."""
104
+ path = get_key_path(subject)
105
+ data = json.loads(path.read_text())
106
+ data["seq"] += 1
107
+ path.write_text(json.dumps(data, indent=2))
108
+ return data["seq"]
109
+
110
+
111
+ def get_public_key(subject: str) -> bytes:
112
+ """Get just the public key (no decryption needed)."""
113
+ path = get_key_path(subject)
114
+ if not path.exists():
115
+ raise KeyNotFound(f"No keys found for {subject}")
116
+ data = json.loads(path.read_text())
117
+ return b64url_decode(data["public_key"])
moss/sequence.py ADDED
@@ -0,0 +1,30 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ MOSS_DIR = Path.home() / ".moss"
5
+ SEQ_DIR = MOSS_DIR / "seq"
6
+
7
+
8
+ def get_last_seen_seq(subject: str, key_version: int) -> int:
9
+ """Get last seen sequence for (subject, key_version)."""
10
+ path = SEQ_DIR / f"{subject.replace(':', '_')}_{key_version}.json"
11
+ if not path.exists():
12
+ return 0
13
+ data = json.loads(path.read_text())
14
+ return data.get("last_seq", 0)
15
+
16
+
17
+ def update_last_seen_seq(subject: str, key_version: int, seq: int):
18
+ """Update last seen sequence."""
19
+ SEQ_DIR.mkdir(parents=True, exist_ok=True)
20
+ path = SEQ_DIR / f"{subject.replace(':', '_')}_{key_version}.json"
21
+ path.write_text(json.dumps({"last_seq": seq}))
22
+
23
+
24
+ def check_and_update_seq(subject: str, key_version: int, seq: int) -> bool:
25
+ """Check if seq is valid (greater than last seen) and update. Returns True if valid."""
26
+ last = get_last_seen_seq(subject, key_version)
27
+ if seq <= last:
28
+ return False
29
+ update_last_seen_seq(subject, key_version, seq)
30
+ return True
moss/subject.py ADDED
@@ -0,0 +1,183 @@
1
+ import re
2
+ import time
3
+ from dataclasses import dataclass
4
+ from typing import Any, Optional, Union
5
+
6
+ from .crypto import MossCrypto
7
+ from .canonical import cjjson, payload_hash
8
+ from .encoding import b64url_encode, b64url_decode
9
+ from .keystore import save_keys, load_keys, increment_seq, get_public_key
10
+ from .sequence import check_and_update_seq
11
+ from .envelope import Envelope
12
+ from .errors import InvalidSubject
13
+
14
+ SPEC = "moss-0001"
15
+ VERSION = 1
16
+ SUBJECT_PATTERN = re.compile(r"^moss:[a-z0-9_-]+:[a-z0-9_-]+$")
17
+
18
+
19
+ @dataclass
20
+ class VerifyResult:
21
+ valid: bool
22
+ subject: Optional[str] = None
23
+ payload_hash: Optional[str] = None
24
+ reason: Optional[str] = None
25
+ error_code: Optional[str] = None
26
+
27
+
28
+ class Subject:
29
+ def __init__(self, subject: str, secret_key: bytes, public_key: bytes, key_version: int):
30
+ self.subject = subject
31
+ self.secret_key = secret_key
32
+ self.public_key = public_key
33
+ self.key_version = key_version
34
+
35
+ @classmethod
36
+ def create(cls, subject: str) -> "Subject":
37
+ """Create a new subject with fresh ML-DSA-44 keypair."""
38
+ if not SUBJECT_PATTERN.match(subject):
39
+ raise InvalidSubject(
40
+ f"Invalid subject format: {subject}. "
41
+ f"Expected: moss:namespace:name (lowercase alphanumeric, hyphens, underscores)"
42
+ )
43
+
44
+ pk, sk = MossCrypto.keygen()
45
+ save_keys(subject, pk, sk, key_version=1)
46
+
47
+ return cls(subject=subject, secret_key=sk, public_key=pk, key_version=1)
48
+
49
+ @classmethod
50
+ def load(cls, subject: str) -> "Subject":
51
+ """Load existing subject from local keystore."""
52
+ data = load_keys(subject)
53
+ return cls(
54
+ subject=data["subject"],
55
+ secret_key=data["secret_key"],
56
+ public_key=data["public_key"],
57
+ key_version=data["key_version"]
58
+ )
59
+
60
+ def sign(self, payload: Any) -> Envelope:
61
+ """Sign a payload and return an envelope."""
62
+ seq = increment_seq(self.subject)
63
+ issued_at = int(time.time())
64
+ p_hash = payload_hash(payload)
65
+
66
+ signed_bytes_obj = {
67
+ "spec": SPEC,
68
+ "version": VERSION,
69
+ "alg": MossCrypto.ALG,
70
+ "subject": self.subject,
71
+ "key_version": self.key_version,
72
+ "seq": seq,
73
+ "issued_at": issued_at,
74
+ "payload_hash": p_hash
75
+ }
76
+ signed_bytes = cjjson(signed_bytes_obj)
77
+
78
+ signature = MossCrypto.sign(self.secret_key, signed_bytes)
79
+
80
+ return Envelope(
81
+ spec=SPEC,
82
+ version=VERSION,
83
+ alg=MossCrypto.ALG,
84
+ subject=self.subject,
85
+ key_version=self.key_version,
86
+ seq=seq,
87
+ issued_at=issued_at,
88
+ payload_hash=p_hash,
89
+ signature=b64url_encode(signature)
90
+ )
91
+
92
+ @staticmethod
93
+ def verify(
94
+ envelope: Union[Envelope, dict],
95
+ payload: Any = None,
96
+ public_key: Optional[bytes] = None,
97
+ check_replay: bool = True
98
+ ) -> VerifyResult:
99
+ """
100
+ Verify an envelope.
101
+
102
+ Args:
103
+ envelope: Envelope object or dict
104
+ payload: Original payload (optional, for hash verification)
105
+ public_key: Public key bytes (optional, will load from keystore if not provided)
106
+ check_replay: Whether to check/update sequence numbers
107
+
108
+ Returns:
109
+ VerifyResult with valid=True/False and details
110
+ """
111
+ if isinstance(envelope, dict):
112
+ try:
113
+ envelope = Envelope.from_dict(envelope)
114
+ except (KeyError, TypeError) as e:
115
+ return VerifyResult(
116
+ valid=False,
117
+ reason=f"Malformed envelope: {e}",
118
+ error_code="MOSS_ERR_003"
119
+ )
120
+
121
+ if envelope.spec != SPEC:
122
+ return VerifyResult(
123
+ valid=False,
124
+ reason=f"Unknown spec: {envelope.spec}",
125
+ error_code="MOSS_ERR_003"
126
+ )
127
+
128
+ if payload is not None:
129
+ computed_hash = payload_hash(payload)
130
+ if computed_hash != envelope.payload_hash:
131
+ return VerifyResult(
132
+ valid=False,
133
+ subject=envelope.subject,
134
+ reason=f"Payload hash mismatch: expected {envelope.payload_hash}, got {computed_hash}",
135
+ error_code="MOSS_ERR_005"
136
+ )
137
+
138
+ if check_replay:
139
+ if not check_and_update_seq(envelope.subject, envelope.key_version, envelope.seq):
140
+ return VerifyResult(
141
+ valid=False,
142
+ subject=envelope.subject,
143
+ reason=f"Replay detected: seq {envelope.seq} already seen for {envelope.subject}",
144
+ error_code="MOSS_ERR_006"
145
+ )
146
+
147
+ if public_key is None:
148
+ try:
149
+ public_key = get_public_key(envelope.subject)
150
+ except Exception as e:
151
+ return VerifyResult(
152
+ valid=False,
153
+ subject=envelope.subject,
154
+ reason=f"Could not load public key: {e}",
155
+ error_code="MOSS_ERR_002"
156
+ )
157
+
158
+ signed_bytes_obj = {
159
+ "spec": envelope.spec,
160
+ "version": envelope.version,
161
+ "alg": envelope.alg,
162
+ "subject": envelope.subject,
163
+ "key_version": envelope.key_version,
164
+ "seq": envelope.seq,
165
+ "issued_at": envelope.issued_at,
166
+ "payload_hash": envelope.payload_hash
167
+ }
168
+ signed_bytes = cjjson(signed_bytes_obj)
169
+
170
+ signature = b64url_decode(envelope.signature)
171
+ if not MossCrypto.verify(public_key, signed_bytes, signature):
172
+ return VerifyResult(
173
+ valid=False,
174
+ subject=envelope.subject,
175
+ reason="Invalid signature",
176
+ error_code="MOSS_ERR_004"
177
+ )
178
+
179
+ return VerifyResult(
180
+ valid=True,
181
+ subject=envelope.subject,
182
+ payload_hash=envelope.payload_hash
183
+ )
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: moss-sdk
3
+ Version: 0.1.0
4
+ Summary: Cryptographic identity and signed outputs for AI agents
5
+ Project-URL: Homepage, https://mosscomputing.com
6
+ Project-URL: Repository, https://github.com/mosscomputing/moss
7
+ Author: MOSS Computing
8
+ License-Expression: MIT
9
+ Keywords: agents,ai,cryptography,identity,post-quantum,signing
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Requires-Python: >=3.9
19
+ Requires-Dist: cryptography>=41.0.0
20
+ Requires-Dist: dilithium-py>=0.1.0
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
23
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
24
+ Description-Content-Type: text/markdown
25
+
26
+ # MOSS
27
+
28
+ **Know exactly which agent produced what.**
29
+
30
+ MOSS (Machine to Machine Operation Security System) is a cryptographic signing protocol for AI agents. Every agent gets an identity. Every output gets a signature. Verifiable, tamper-proof, reproducible.
31
+
32
+ [![CI](https://github.com/mosscomputing/moss/actions/workflows/ci.yml/badge.svg)](https://github.com/mosscomputing/moss/actions/workflows/ci.yml)
33
+ [![PyPI version](https://badge.fury.io/py/moss-sdk.svg)](https://badge.fury.io/py/moss-sdk)
34
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ pip install moss-sdk
40
+ ```
41
+
42
+ ## Quick Start
43
+
44
+ ```python
45
+ from moss import Subject
46
+
47
+ # Create an agent identity
48
+ agent = Subject.create("moss:dev:my-agent")
49
+
50
+ # Sign any output
51
+ envelope = agent.sign({"action": "approved", "amount": 500})
52
+
53
+ # Verify
54
+ result = Subject.verify(envelope)
55
+ assert result.valid
56
+ print(result.subject) # moss:dev:my-agent
57
+ ```
58
+
59
+ ## Why MOSS?
60
+
61
+ Multi-agent pipeline breaks, four agents touched it, no idea which one — you've been there. MOSS gives every agent an identity and signs every output so you can trace, diff, and verify.
62
+
63
+ **Security guarantees:**
64
+ - **Identity** — Unique subject per agent, bound to cryptographic key pair
65
+ - **Integrity** — Payload hash detects any modification
66
+ - **Replay protection** — Sequence numbers prevent reuse
67
+ - **Post-quantum** — ML-DSA-44 (FIPS 204) signatures survive quantum
68
+
69
+ ## CLI
70
+
71
+ ```bash
72
+ # Create a subject
73
+ moss subject create moss:dev:my-agent
74
+
75
+ # Sign a payload
76
+ echo '{"action": "test"}' | moss sign moss:dev:my-agent - > envelope.json
77
+
78
+ # Verify
79
+ moss verify payload.json envelope.json
80
+
81
+ # Compare envelopes
82
+ moss diff envelope1.json envelope2.json
83
+ ```
84
+
85
+ ## Framework Integrations
86
+
87
+ ```bash
88
+ pip install moss-sdk-crewai # CrewAI
89
+ pip install moss-sdk-autogen # AutoGen
90
+ pip install moss-sdk-langgraph # LangGraph
91
+ pip install moss-sdk-langchain # LangChain
92
+ ```
93
+
94
+ ## Specification
95
+
96
+ MOSS implements the `moss-0001` specification. See [SPEC.md](SPEC.md) for the full protocol specification.
97
+
98
+ ### Envelope Format
99
+
100
+ ```json
101
+ {
102
+ "spec": "moss-0001",
103
+ "version": 1,
104
+ "alg": "ML-DSA-44",
105
+ "subject": "moss:acme:order-bot",
106
+ "key_version": 1,
107
+ "seq": 42,
108
+ "issued_at": 1733200000,
109
+ "payload_hash": "<base64url(SHA-256(canonical(payload)))>",
110
+ "signature": "<base64url(ML-DSA-44 signature)>"
111
+ }
112
+ ```
113
+
114
+ ### Verification Procedure
115
+
116
+ 1. Require `spec == "moss-0001"`
117
+ 2. Compute `hash = base64url(SHA-256(canonical(payload)))`
118
+ 3. Assert `hash == envelope.payload_hash`
119
+ 4. Resolve `(subject, key_version) → public_key`
120
+ 5. Verify `ML-DSA-44.verify(public_key, canonical(signed_bytes), signature)`
121
+
122
+ ## Local-First
123
+
124
+ MOSS works entirely offline. Keys are stored locally at `~/.moss/keys/`. No account required. No cloud. No tracking.
125
+
126
+ To encrypt keys at rest, set:
127
+ ```bash
128
+ export MOSS_KEY_PASSPHRASE="your-secure-passphrase"
129
+ ```
130
+
131
+ ## What MOSS Collects
132
+
133
+ **In the envelope:** `subject`, `payload_hash` (not the payload itself), `seq`, `issued_at`, `signature`
134
+
135
+ **Never:** Your payload content, LLM prompts/responses, user data, PII, telemetry
136
+
137
+ ## Technical Details
138
+
139
+ | Component | Value |
140
+ |-----------|-------|
141
+ | Signature | ML-DSA-44 (FIPS 204, Dilithium2) |
142
+ | Hash | SHA-256 |
143
+ | Encoding | base64url (no padding) |
144
+ | Canonicalization | RFC 8785 (JCS) |
145
+ | Key encryption | AES-256-GCM + Scrypt |
146
+ | Public key size | 1312 bytes |
147
+ | Signature size | 2420 bytes |
148
+ | Overhead | <1ms sign, <1ms verify, ~2KB envelope |
149
+
150
+ ## Contributing
151
+
152
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
153
+
154
+ ## Security
155
+
156
+ See [SECURITY.md](SECURITY.md) for reporting vulnerabilities.
157
+
158
+ ## License
159
+
160
+ MIT — see [LICENSE](LICENSE)
161
+
162
+ ## Links
163
+
164
+ - **Website:** [mosscomputing.com](https://mosscomputing.com)
165
+ - **Spec:** [SPEC.md](SPEC.md)
166
+ - **Discord:** [Join](https://discord.gg/2QewJsBc)
@@ -0,0 +1,15 @@
1
+ moss/__init__.py,sha256=pGa_nS4JAM8w6KCU9qJXZn8m4d5rtJVYDfI7-0OGPeg,519
2
+ moss/__main__.py,sha256=MSmt_5Xg84uHqzTN38JwgseJK8rsJn_11A8WD99VtEo,61
3
+ moss/canonical.py,sha256=A8O_Chu1n49u4u_TfabmJqp3Fl4Parqi0s4N5EK2Fhw,684
4
+ moss/cli.py,sha256=08aHw_LzhrMQt6fQIw8VImRrVt1G-sJXTY26HajMHho,4012
5
+ moss/crypto.py,sha256=d4lhq0KfYwd4P7GQQsw-_LyToSK2Hdpq-sFNxAxRY-4,966
6
+ moss/encoding.py,sha256=zn5ndDydH_-S5cbxz5RFKTZeKb-bW7e_PBtcmnByNLk,378
7
+ moss/envelope.py,sha256=ucjDa2RViOv1OiRFk5_0Dvl776w2n6w0kckc9qPPpAY,1017
8
+ moss/errors.py,sha256=KNO6haC1Sw6R453RTzafZgZbrwrhM7ZjhLIo2HPu3Fw,830
9
+ moss/keystore.py,sha256=z6_0EtVuOY4u7UAbT-cAQSGWIqilclcji7vS1vD37sQ,3702
10
+ moss/sequence.py,sha256=sb5u1Ead_5A_KI99NdFR-xvKQKtEPgagi563jBqeH_s,1018
11
+ moss/subject.py,sha256=ojCXV--UlZHEa_ZJtHIJKpVPPArUb03FAqC7QOkOAS0,6120
12
+ moss_sdk-0.1.0.dist-info/METADATA,sha256=zokkRsXiTCS-PyA2h4sP4zgFOkftTRkb_Dy-nwMetpM,4813
13
+ moss_sdk-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
14
+ moss_sdk-0.1.0.dist-info/entry_points.txt,sha256=kc1MH_7F5_YF0TB7rUTdb33A9KachU9DZGmlNBBMA9Y,39
15
+ moss_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ moss = moss.cli:main