an atproto pds written in F# (.NET 9) 🦒
pds fsharp giraffe dotnet atproto

feat: CID generation and canonical DAG-CBOR encoding

Changed files
+191 -15
PDSharp.Core
PDSharp.Tests
+70
PDSharp.Core/Cid.fs
··· 1 + namespace PDSharp.Core 2 + 3 + open System 4 + open System.Text 5 + 6 + /// Minimal Base32 (RFC 4648 Lowercase) 7 + module Base32Encoding = 8 + let private alphabet = "abcdefghijklmnopqrstuvwxyz234567" 9 + 10 + let ToString (data : byte[]) : string = 11 + if data.Length = 0 then 12 + "" 13 + else 14 + let mutable i = 0 15 + let mutable index = 0 16 + let mutable digit = 0 17 + let mutable currByte = 0 18 + let mutable nextByte = 0 19 + let sb = StringBuilder((data.Length + 7) * 8 / 5) 20 + 21 + while i < data.Length do 22 + currByte <- (int data.[i]) &&& 0xFF 23 + 24 + if index > 3 then 25 + if (i + 1) < data.Length then 26 + nextByte <- (int data.[i + 1]) &&& 0xFF 27 + else 28 + nextByte <- 0 29 + 30 + digit <- currByte &&& (0xFF >>> index) 31 + index <- (index + 5) % 8 32 + digit <- digit <<< index 33 + digit <- digit ||| (nextByte >>> (8 - index)) 34 + i <- i + 1 35 + else 36 + digit <- currByte >>> 8 - (index + 5) &&& 0x1F 37 + index <- (index + 5) % 8 38 + 39 + if index = 0 then 40 + i <- i + 1 41 + 42 + sb.Append(alphabet.[digit]) |> ignore 43 + 44 + sb.ToString() 45 + 46 + /// Basic CID implementation for AT Protocol (CIDv1 + dag-cbor + sha2-256) 47 + /// 48 + /// Constants for ATProto defaults: 49 + /// - Version 1 (0x01) 50 + /// - Codec: dag-cbor (0x71) 51 + /// - Hash: sha2-256 (0x12) - Length 32 (0x20) 52 + [<Struct>] 53 + type Cid = 54 + val Bytes : byte[] 55 + new(bytes : byte[]) = { Bytes = bytes } 56 + 57 + static member FromHash(hash : byte[]) = 58 + if hash.Length <> 32 then 59 + failwith "Hash must be 32 bytes (sha2-256)" 60 + 61 + let cidBytes = Array.zeroCreate<byte> 36 62 + cidBytes.[0] <- 0x01uy 63 + cidBytes.[1] <- 0x71uy 64 + cidBytes.[2] <- 0x12uy 65 + cidBytes.[3] <- 0x20uy 66 + Array.Copy(hash, 0, cidBytes, 4, 32) 67 + Cid cidBytes 68 + 69 + override this.ToString() = 70 + "b" + Base32Encoding.ToString(this.Bytes)
+83
PDSharp.Core/DagCbor.fs
··· 1 + namespace PDSharp.Core 2 + 3 + open System 4 + open System.Collections.Generic 5 + open System.Formats.Cbor 6 + open System.IO 7 + open System.Text 8 + 9 + module DagCbor = 10 + type SortKey = { Length : int; Bytes : byte[] } 11 + 12 + let private getSortKey (key : string) = 13 + let bytes = Encoding.UTF8.GetBytes(key) 14 + { Length = bytes.Length; Bytes = bytes } 15 + 16 + let private compareKeys (a : string) (b : string) = 17 + let ka = getSortKey a 18 + let kb = getSortKey b 19 + 20 + if ka.Length <> kb.Length then 21 + ka.Length.CompareTo kb.Length 22 + else 23 + let mutable res = 0 24 + let mutable i = 0 25 + 26 + while res = 0 && i < ka.Bytes.Length do 27 + res <- ka.Bytes.[i].CompareTo(kb.Bytes.[i]) 28 + i <- i + 1 29 + 30 + res 31 + 32 + let rec private writeItem (writer : CborWriter) (item : obj) = 33 + match item with 34 + | null -> writer.WriteNull() 35 + | :? bool as b -> writer.WriteBoolean(b) 36 + | :? int as i -> writer.WriteInt32(i) 37 + | :? int64 as l -> writer.WriteInt64(l) 38 + | :? string as s -> writer.WriteTextString(s) 39 + | :? (byte[]) as b -> writer.WriteByteString(b) 40 + | :? Cid as c -> 41 + let tag = LanguagePrimitives.EnumOfValue<uint64, CborTag>(42UL) 42 + writer.WriteTag(tag) 43 + let rawCid = c.Bytes 44 + let linkBytes = Array.zeroCreate<byte> (rawCid.Length + 1) 45 + linkBytes.[0] <- 0x00uy 46 + Array.Copy(rawCid, 0, linkBytes, 1, rawCid.Length) 47 + writer.WriteByteString(linkBytes) 48 + 49 + | :? Map<string, obj> as m -> 50 + let keys = m |> Map.toList |> List.map fst |> List.sortWith compareKeys 51 + writer.WriteStartMap(keys.Length) 52 + 53 + for k in keys do 54 + writer.WriteTextString(k) 55 + writeItem writer (m.[k]) 56 + 57 + writer.WriteEndMap() 58 + 59 + | :? IDictionary<string, obj> as d -> 60 + let keys = d.Keys |> Seq.toList |> List.sortWith compareKeys 61 + writer.WriteStartMap(d.Count) 62 + 63 + for k in keys do 64 + writer.WriteTextString(k) 65 + writeItem writer (d.[k]) 66 + 67 + writer.WriteEndMap() 68 + 69 + | :? seq<obj> as l -> 70 + let arr = l |> Seq.toArray 71 + writer.WriteStartArray(arr.Length) 72 + 73 + for x in arr do 74 + writeItem writer x 75 + 76 + writer.WriteEndArray() 77 + 78 + | _ -> failwith $"Unsupported type for DAG-CBOR: {item.GetType().Name}" 79 + 80 + let encode (data : obj) : byte[] = 81 + let writer = new CborWriter(CborConformanceMode.Strict, false, false) 82 + writeItem writer data 83 + writer.Encode()
+7 -4
PDSharp.Core/PDSharp.Core.fsproj
··· 5 5 </PropertyGroup> 6 6 7 7 <ItemGroup> 8 - <Compile Include="Config.fs"/> 9 - <Compile Include="Crypto.fs"/> 10 - <Compile Include="DidResolver.fs"/> 11 - <Compile Include="Library.fs"/> 8 + <Compile Include="Config.fs" /> 9 + <Compile Include="Cid.fs" /> 10 + <Compile Include="DagCbor.fs" /> 11 + <Compile Include="Crypto.fs" /> 12 + <Compile Include="DidResolver.fs" /> 13 + <Compile Include="Library.fs" /> 12 14 </ItemGroup> 13 15 14 16 <ItemGroup> 15 17 <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" /> 18 + <PackageReference Include="System.Formats.Cbor" Version="10.0.1" /> 16 19 </ItemGroup> 17 20 </Project>
+28 -8
PDSharp.Tests/Tests.fs
··· 1 1 module Tests 2 2 3 - open System 4 3 open Xunit 5 4 open PDSharp.Core.Models 6 5 open PDSharp.Core.Config 7 6 open PDSharp.Core.Crypto 7 + open PDSharp.Core 8 8 open PDSharp.Core.DidResolver 9 9 open Org.BouncyCastle.Utilities.Encoders 10 10 open System.Text 11 + open System.Text.Json 11 12 open Org.BouncyCastle.Math 12 - 13 - [<Fact>] 14 - let ``My test`` () = Assert.True(true) 15 13 16 14 [<Fact>] 17 15 let ``Can instantiate AppConfig`` () = ··· 91 89 }""" 92 90 93 91 let doc = 94 - System.Text.Json.JsonSerializer.Deserialize<DidDocument>( 95 - json, 96 - Json.JsonSerializerOptions(PropertyNameCaseInsensitive = true) 97 - ) 92 + JsonSerializer.Deserialize<DidDocument>(json, JsonSerializerOptions(PropertyNameCaseInsensitive = true)) 98 93 99 94 Assert.Equal("did:web:example.com", doc.Id) 100 95 Assert.Single doc.VerificationMethod |> ignore 101 96 Assert.Equal("Multikey", doc.VerificationMethod.Head.Type) 97 + 98 + [<Fact>] 99 + let ``CID Generation from Hash`` () = 100 + let hash = 101 + Hex.Decode("b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9") 102 + 103 + let cid = Cid.FromHash hash 104 + Assert.Equal<byte>(0x01uy, cid.Bytes.[0]) 105 + Assert.Equal<byte>(0x71uy, cid.Bytes.[1]) 106 + Assert.Equal<byte>(0x12uy, cid.Bytes.[2]) 107 + Assert.Equal<byte>(0x20uy, cid.Bytes.[3]) 108 + 109 + [<Fact>] 110 + let ``DAG-CBOR Canonical Sorting`` () = 111 + let m = Map.ofList [ ("b", box 1); ("a", box 2) ] 112 + let encoded = DagCbor.encode m 113 + let hex = Hex.ToHexString encoded 114 + Assert.Equal("a2616102616201", hex) 115 + 116 + [<Fact>] 117 + let ``DAG-CBOR Sorting Length vs Bytes`` () = 118 + let m = Map.ofList [ ("aa", box 1); ("b", box 2) ] 119 + let encoded = DagCbor.encode m 120 + let hex = Hex.ToHexString encoded 121 + Assert.Equal("a261620262616101", hex)
+3 -3
roadmap.txt
··· 17 17 -------------------------------------------------------------------------------- 18 18 Milestone C: DAG-CBOR + CID 19 19 -------------------------------------------------------------------------------- 20 - - Canonical DAG-CBOR encode/decode with IPLD link tagging 21 - - CID creation/parsing (multicodec dag-cbor, sha2-256) 20 + - [x] Canonical DAG-CBOR encode/decode with IPLD link tagging 21 + - [x] CID creation/parsing (multicodec dag-cbor, sha2-256) 22 22 DoD: Record JSON → stable DAG-CBOR bytes → deterministic CID 23 23 -------------------------------------------------------------------------------- 24 24 Milestone D: MST Implementation ··· 70 70 Milestone K: DNS + TLS + Reverse Proxy 71 71 -------------------------------------------------------------------------------- 72 72 - DNS A/AAAA records for PDS hostname 73 - - TLS certs (ACME) via Caddy/Nginx/Traefik 73 + - TLS certs (ACME) via Caddy 74 74 DoD: https://<pds-hostname> responds with valid cert 75 75 -------------------------------------------------------------------------------- 76 76 Milestone L: Deploy PDSharp