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

Configure Feed

Select the types of activity you want to include in your feed.

feat: Implement crypto primitives for SHA-256 and ECDSA

* DID resolution for did:web and did:plc.

+260 -3
+108
PDSharp.Core/Crypto.fs
··· 1 + namespace PDSharp.Core 2 + 3 + open System 4 + open System.Text 5 + open Org.BouncyCastle.Crypto.Digests 6 + open Org.BouncyCastle.Crypto.Signers 7 + open Org.BouncyCastle.Crypto.Parameters 8 + open Org.BouncyCastle.Math 9 + open Org.BouncyCastle.Asn1.X9 10 + open Org.BouncyCastle.Crypto.Generators 11 + open Org.BouncyCastle.Security 12 + open Org.BouncyCastle.Asn1.Sec 13 + 14 + module Crypto = 15 + let sha256 (data : byte[]) : byte[] = 16 + let digest = Sha256Digest() 17 + digest.BlockUpdate(data, 0, data.Length) 18 + let size = digest.GetDigestSize() 19 + let result = Array.zeroCreate<byte> size 20 + digest.DoFinal(result, 0) |> ignore 21 + result 22 + 23 + let sha256Str (input : string) : byte[] = sha256 (Encoding.UTF8.GetBytes(input)) 24 + 25 + type Curve = 26 + | P256 27 + | K256 28 + 29 + let getCurveParams (curve : Curve) = 30 + match curve with 31 + | P256 -> ECNamedCurveTable.GetByName("secp256r1") 32 + | K256 -> ECNamedCurveTable.GetByName("secp256k1") 33 + 34 + let getDomainParams (curve : Curve) = 35 + let ecP = getCurveParams curve 36 + ECDomainParameters(ecP.Curve, ecP.G, ecP.N, ecP.H, ecP.GetSeed()) 37 + 38 + type EcKeyPair = { 39 + PrivateKey : ECPrivateKeyParameters option 40 + PublicKey : ECPublicKeyParameters 41 + Curve : Curve 42 + } 43 + 44 + let generateKey (curve : Curve) : EcKeyPair = 45 + let domainParams = getDomainParams curve 46 + let genParam = ECKeyGenerationParameters(domainParams, SecureRandom()) 47 + let generator = ECKeyPairGenerator() 48 + generator.Init(genParam) 49 + let pair = generator.GenerateKeyPair() 50 + 51 + { 52 + PrivateKey = Some(pair.Private :?> ECPrivateKeyParameters) 53 + PublicKey = (pair.Public :?> ECPublicKeyParameters) 54 + Curve = curve 55 + } 56 + 57 + let enforceLowS (s : BigInteger) (n : BigInteger) : BigInteger = 58 + let halfN = n.ShiftRight(1) 59 + if s.CompareTo(halfN) > 0 then n.Subtract(s) else s 60 + 61 + let sign (key : EcKeyPair) (digest : byte[]) : byte[] = 62 + match key.PrivateKey with 63 + | None -> failwith "Private key required for signing" 64 + | Some privParams -> 65 + let signer = ECDsaSigner() 66 + signer.Init(true, privParams) 67 + let inputs = digest 68 + let signature = signer.GenerateSignature(inputs) 69 + let r = signature.[0] 70 + let s = signature.[1] 71 + 72 + let n = privParams.Parameters.N 73 + let canonicalS = enforceLowS s n 74 + 75 + let to32Bytes (bi : BigInteger) = 76 + let bytes = bi.ToByteArrayUnsigned() 77 + 78 + if bytes.Length > 32 then 79 + failwith "Signature component too large" 80 + 81 + let padded = Array.zeroCreate<byte> 32 82 + Array.Copy(bytes, 0, padded, 32 - bytes.Length, bytes.Length) 83 + padded 84 + 85 + let rBytes = to32Bytes r 86 + let sBytes = to32Bytes canonicalS 87 + Array.append rBytes sBytes 88 + 89 + let verify (key : EcKeyPair) (digest : byte[]) (signature : byte[]) : bool = 90 + if signature.Length <> 64 then 91 + false 92 + else 93 + let rBytes = Array.sub signature 0 32 94 + let sBytes = Array.sub signature 32 32 95 + 96 + let r = BigInteger(1, rBytes) 97 + let s = BigInteger(1, sBytes) 98 + 99 + let domainParams = key.PublicKey.Parameters 100 + let n = domainParams.N 101 + let halfN = n.ShiftRight(1) 102 + 103 + if s.CompareTo(halfN) > 0 then 104 + false 105 + else 106 + let signer = ECDsaSigner() 107 + signer.Init(false, key.PublicKey) 108 + signer.VerifySignature(digest, r, s)
+73
PDSharp.Core/DidResolver.fs
··· 1 + namespace PDSharp.Core 2 + 3 + open System 4 + open System.Net.Http 5 + open System.Text.Json 6 + open System.Text.Json.Serialization 7 + 8 + module DidResolver = 9 + type VerificationMethod = { 10 + [<JsonPropertyName("id")>] 11 + Id : string 12 + [<JsonPropertyName("type")>] 13 + Type : string 14 + [<JsonPropertyName("controller")>] 15 + Controller : string 16 + [<JsonPropertyName("publicKeyMultibase")>] 17 + PublicKeyMultibase : string option 18 + } 19 + 20 + type DidDocument = { 21 + [<JsonPropertyName("id")>] 22 + Id : string 23 + [<JsonPropertyName("verificationMethod")>] 24 + VerificationMethod : VerificationMethod list 25 + } 26 + 27 + let private httpClient = new HttpClient() 28 + 29 + let private fetchJson<'T> (url : string) : Async<'T option> = async { 30 + try 31 + let! response = httpClient.GetAsync url |> Async.AwaitTask 32 + 33 + if response.IsSuccessStatusCode then 34 + let! stream = response.Content.ReadAsStreamAsync() |> Async.AwaitTask 35 + let options = JsonSerializerOptions(PropertyNameCaseInsensitive = true) 36 + let! doc = JsonSerializer.DeserializeAsync<'T>(stream, options).AsTask() |> Async.AwaitTask 37 + return Some doc 38 + else 39 + return None 40 + with _ -> 41 + return None 42 + } 43 + 44 + let resolveDidWeb (did : string) : Async<DidDocument option> = async { 45 + let parts = did.Split(':') 46 + 47 + if parts.Length < 3 then 48 + return None 49 + else 50 + let domain = parts.[2] 51 + 52 + let url = 53 + if domain = "localhost" then 54 + "http://localhost:5000/.well-known/did.json" 55 + else 56 + $"https://{domain}/.well-known/did.json" 57 + 58 + return! fetchJson<DidDocument> url 59 + } 60 + 61 + let resolveDidPlc (did : string) : Async<DidDocument option> = async { 62 + let url = $"https://plc.directory/{did}" 63 + return! fetchJson<DidDocument> url 64 + } 65 + 66 + let resolve (did : string) : Async<DidDocument option> = async { 67 + if did.StartsWith("did:web:") then 68 + return! resolveDidWeb did 69 + elif did.StartsWith("did:plc:") then 70 + return! resolveDidPlc did 71 + else 72 + return None 73 + }
+5 -1
PDSharp.Core/PDSharp.Core.fsproj
··· 1 1 <Project Sdk="Microsoft.NET.Sdk"> 2 - 3 2 <PropertyGroup> 4 3 <TargetFramework>net9.0</TargetFramework> 5 4 <GenerateDocumentationFile>true</GenerateDocumentationFile> ··· 7 6 8 7 <ItemGroup> 9 8 <Compile Include="Config.fs"/> 9 + <Compile Include="Crypto.fs"/> 10 + <Compile Include="DidResolver.fs"/> 10 11 <Compile Include="Library.fs"/> 11 12 </ItemGroup> 12 13 14 + <ItemGroup> 15 + <PackageReference Include="BouncyCastle.Cryptography" Version="2.6.2" /> 16 + </ItemGroup> 13 17 </Project>
+72
PDSharp.Tests/Tests.fs
··· 4 4 open Xunit 5 5 open PDSharp.Core.Models 6 6 open PDSharp.Core.Config 7 + open PDSharp.Core.Crypto 8 + open PDSharp.Core.DidResolver 9 + open Org.BouncyCastle.Utilities.Encoders 10 + open System.Text 11 + open Org.BouncyCastle.Math 7 12 8 13 [<Fact>] 9 14 let ``My test`` () = Assert.True(true) ··· 27 32 28 33 Assert.Equal("did:web:example.com", response.did) 29 34 Assert.Equal(1, response.availableUserDomains.Length) 35 + 36 + [<Fact>] 37 + let ``SHA-256 Hashing correct`` () = 38 + let input = "hello world" 39 + let hash = sha256Str input 40 + let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" 41 + let actual = Hex.ToHexString(hash) 42 + Assert.Equal(expected, actual) 43 + 44 + [<Fact>] 45 + let ``ECDSA P-256 Sign and Verify`` () = 46 + let keyPair = generateKey P256 47 + let data = Encoding.UTF8.GetBytes("test message") 48 + let hash = sha256 data 49 + 50 + let signature = sign keyPair hash 51 + Assert.True(signature.Length = 64, "Signature should be 64 bytes (R|S)") 52 + 53 + let valid = verify keyPair hash signature 54 + Assert.True(valid, "Signature verification failed") 55 + 56 + [<Fact>] 57 + let ``ECDSA K-256 Sign and Verify`` () = 58 + let keyPair = generateKey K256 59 + let data = Encoding.UTF8.GetBytes("test k256") 60 + let hash = sha256 data 61 + 62 + let signature = sign keyPair hash 63 + Assert.True(signature.Length = 64, "Signature should be 64 bytes") 64 + 65 + let valid = verify keyPair hash signature 66 + Assert.True(valid, "Signature verification failed") 67 + 68 + [<Fact>] 69 + let ``Low-S Enforcement Logic`` () = 70 + let n = 71 + BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141", 16) // secp256k1 N 72 + 73 + let halfN = n.ShiftRight(1) 74 + let highS = halfN.Add(BigInteger.One) 75 + 76 + let lowS = enforceLowS highS n 77 + Assert.True(lowS.CompareTo halfN <= 0, "S value should be <= N/2") 78 + Assert.Equal(n.Subtract highS, lowS) 79 + 80 + [<Fact>] 81 + let ``DidDocument JSON deserialization`` () = 82 + let json = 83 + """{ 84 + "id": "did:web:example.com", 85 + "verificationMethod": [{ 86 + "id": "did:web:example.com#atproto", 87 + "type": "Multikey", 88 + "controller": "did:web:example.com", 89 + "publicKeyMultibase": "zQ3sh..." 90 + }] 91 + }""" 92 + 93 + let doc = 94 + System.Text.Json.JsonSerializer.Deserialize<DidDocument>( 95 + json, 96 + Json.JsonSerializerOptions(PropertyNameCaseInsensitive = true) 97 + ) 98 + 99 + Assert.Equal("did:web:example.com", doc.Id) 100 + Assert.Single doc.VerificationMethod |> ignore 101 + Assert.Equal("Multikey", doc.VerificationMethod.Head.Type)
+2 -2
roadmap.txt
··· 11 11 -------------------------------------------------------------------------------- 12 12 Milestone B: Identity + Crypto Primitives 13 13 -------------------------------------------------------------------------------- 14 - - DID document fetch/parse for signing key and PDS endpoint 15 - - SHA-256 hashing, ECDSA sign/verify (p256 + k256), low-S enforcement 14 + - [x] DID document fetch/parse for signing key and PDS endpoint 15 + - [x] SHA-256 hashing, ECDSA sign/verify (p256 + k256), low-S enforcement 16 16 DoD: Sign and verify atproto commit hash with low-S 17 17 -------------------------------------------------------------------------------- 18 18 Milestone C: DAG-CBOR + CID