atproto libraries implementation in ocaml
at main 6.0 kB view raw
1(** Repository Commit for AT Protocol. 2 3 A commit is a signed snapshot of a repository. It contains the DID of the 4 repository owner, the root CID of the MST data tree, a revision TID, and a 5 signature over the commit body. 6 7 AT Protocol uses v3 commits with the following structure: 8 - did: repository DID (string) 9 - version: always 3 (int) 10 - data: CID of MST root 11 - rev: revision TID (string) 12 - prev: optional CID of previous commit 13 - sig: raw signature bytes (64 bytes for K-256) *) 14 15open Atproto_ipld 16open Atproto_crypto 17 18type error = 19 [ `Invalid_commit of string 20 | `Invalid_signature 21 | `Verification_failed 22 | `Missing_field of string 23 | `Decode_error of string ] 24 25let pp_error fmt = function 26 | `Invalid_commit msg -> Format.fprintf fmt "invalid commit: %s" msg 27 | `Invalid_signature -> Format.fprintf fmt "invalid signature" 28 | `Verification_failed -> Format.fprintf fmt "signature verification failed" 29 | `Missing_field f -> Format.fprintf fmt "missing required field: %s" f 30 | `Decode_error msg -> Format.fprintf fmt "decode error: %s" msg 31 32let error_to_string e = Format.asprintf "%a" pp_error e 33 34(** AT Protocol commit version (v3) *) 35let commit_version = 3 36 37type t = { 38 did : string; (** Repository DID *) 39 version : int; (** Commit version (always 3) *) 40 data : Cid.t; (** MST root CID *) 41 rev : string; (** Revision TID *) 42 prev : Cid.t option; (** Previous commit CID *) 43 sig_ : string; (** Signature bytes (64 bytes) *) 44} 45 46(** Encode an unsigned commit body to DAG-CBOR. The unsigned commit is used for 47 signing - it contains all fields except sig. *) 48let encode_unsigned ~did ~data ~rev ?prev () = 49 let fields = 50 [ 51 ("did", Dag_cbor.String did); 52 ("data", Dag_cbor.Link data); 53 ("rev", Dag_cbor.String rev); 54 ("version", Dag_cbor.Int (Int64.of_int commit_version)); 55 ] 56 in 57 let fields = 58 match prev with 59 | Some cid -> ("prev", Dag_cbor.Link cid) :: fields 60 | None -> fields 61 in 62 Dag_cbor.encode (Dag_cbor.Map fields) 63 64(** Encode a signed commit to DAG-CBOR *) 65let to_dag_cbor (commit : t) : string = 66 let fields = 67 [ 68 ("did", Dag_cbor.String commit.did); 69 ("data", Dag_cbor.Link commit.data); 70 ("rev", Dag_cbor.String commit.rev); 71 ("sig", Dag_cbor.Bytes commit.sig_); 72 ("version", Dag_cbor.Int (Int64.of_int commit.version)); 73 ] 74 in 75 let fields = 76 match commit.prev with 77 | Some cid -> ("prev", Dag_cbor.Link cid) :: fields 78 | None -> fields 79 in 80 Dag_cbor.encode (Dag_cbor.Map fields) 81 82(** Decode a commit from DAG-CBOR *) 83let of_dag_cbor (data : string) : (t, error) result = 84 match Dag_cbor.decode data with 85 | Error e -> Error (`Decode_error (Dag_cbor.error_to_string e)) 86 | Ok value -> ( 87 match value with 88 | Dag_cbor.Map pairs -> ( 89 let find_string key = 90 List.find_map 91 (fun (k, v) -> 92 if k = key then 93 match v with Dag_cbor.String s -> Some s | _ -> None 94 else None) 95 pairs 96 in 97 let find_int key = 98 List.find_map 99 (fun (k, v) -> 100 if k = key then 101 match v with 102 | Dag_cbor.Int i -> Some (Int64.to_int i) 103 | _ -> None 104 else None) 105 pairs 106 in 107 let find_link key = 108 List.find_map 109 (fun (k, v) -> 110 if k = key then 111 match v with Dag_cbor.Link cid -> Some cid | _ -> None 112 else None) 113 pairs 114 in 115 let find_bytes key = 116 List.find_map 117 (fun (k, v) -> 118 if k = key then 119 match v with Dag_cbor.Bytes b -> Some b | _ -> None 120 else None) 121 pairs 122 in 123 match 124 ( find_string "did", 125 find_int "version", 126 find_link "data", 127 find_string "rev", 128 find_bytes "sig" ) 129 with 130 | Some did, Some version, Some data, Some rev, Some sig_ -> 131 let prev = find_link "prev" in 132 Ok { did; version; data; rev; prev; sig_ } 133 | None, _, _, _, _ -> Error (`Missing_field "did") 134 | _, None, _, _, _ -> Error (`Missing_field "version") 135 | _, _, None, _, _ -> Error (`Missing_field "data") 136 | _, _, _, None, _ -> Error (`Missing_field "rev") 137 | _, _, _, _, None -> Error (`Missing_field "sig")) 138 | _ -> Error (`Invalid_commit "expected map")) 139 140(** Create a new signed commit using K-256 key. 141 142 The signing process: 1. Encode unsigned commit as DAG-CBOR 2. SHA-256 hash 143 the bytes 3. Sign hash with K-256 key (produces low-S signature) 4. Add 144 signature to commit *) 145let create ~did ~data ~rev ?prev ~(key : K256.private_key) () : t = 146 (* Encode unsigned commit *) 147 let unsigned = encode_unsigned ~did ~data ~rev ?prev () in 148 (* Sign the unsigned commit bytes directly (K256.sign will hash it) *) 149 let sig_ = K256.sign key unsigned in 150 { did; version = commit_version; data; rev; prev; sig_ } 151 152(** Verify a commit signature against a public key. 153 154 Returns Ok () if the signature is valid, Error otherwise. *) 155let verify (commit : t) ~(public_key : K256.public_key) : (unit, error) result = 156 (* Reconstruct the unsigned commit *) 157 let unsigned = 158 encode_unsigned ~did:commit.did ~data:commit.data ~rev:commit.rev 159 ?prev:commit.prev () 160 in 161 (* Verify signature *) 162 match K256.verify public_key unsigned commit.sig_ with 163 | Ok () -> Ok () 164 | Error _ -> Error `Verification_failed 165 166(** Get the CID of a commit *) 167let cid (commit : t) : Cid.t = 168 let data = to_dag_cbor commit in 169 Cid.of_dag_cbor data 170 171(** Check if a commit is valid (version check, signature length, etc.) *) 172let is_valid (commit : t) : bool = 173 commit.version = commit_version && String.length commit.sig_ = 64