Skip to content

Commit

Permalink
Merge pull request joemphilips#101 from joemphilips/macaroon
Browse files Browse the repository at this point in the history
add package for macaroon and its test
  • Loading branch information
joemphilips committed Jun 8, 2020
2 parents bb4b62a + 5d228cd commit 176587e
Show file tree
Hide file tree
Showing 44 changed files with 2,581 additions and 5 deletions.
30 changes: 30 additions & 0 deletions DotNetLightning.sln
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "DotNetLightning.Client.Test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DotNetLightning.Integration.Tests", "tests\DotNetLightning.Integration.Tests\DotNetLightning.Integration.Tests.csproj", "{AA2DA4CA-FE39-431B-84C2-1C99E3B282D8}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Macaroons", "src\Macaroons\Macaroons.csproj", "{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Macaroons.Tests", "tests\Macaroons.Tests\Macaroons.Tests.csproj", "{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -288,6 +292,30 @@ Global
{AA2DA4CA-FE39-431B-84C2-1C99E3B282D8}.Release|x64.Build.0 = Release|Any CPU
{AA2DA4CA-FE39-431B-84C2-1C99E3B282D8}.Release|x86.ActiveCfg = Release|Any CPU
{AA2DA4CA-FE39-431B-84C2-1C99E3B282D8}.Release|x86.Build.0 = Release|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Debug|x64.ActiveCfg = Debug|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Debug|x64.Build.0 = Debug|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Debug|x86.ActiveCfg = Debug|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Debug|x86.Build.0 = Debug|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Release|Any CPU.Build.0 = Release|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Release|x64.ActiveCfg = Release|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Release|x64.Build.0 = Release|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Release|x86.ActiveCfg = Release|Any CPU
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A}.Release|x86.Build.0 = Release|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Debug|x64.ActiveCfg = Debug|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Debug|x64.Build.0 = Debug|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Debug|x86.ActiveCfg = Debug|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Debug|x86.Build.0 = Debug|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Release|Any CPU.Build.0 = Release|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Release|x64.ActiveCfg = Release|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Release|x64.Build.0 = Release|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Release|x86.ActiveCfg = Release|Any CPU
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{594F2275-AF8E-4AFC-B8DC-7D6048C872CA} = {84B95569-6BD8-48B4-A942-CBBC9462B6D4}
Expand All @@ -310,5 +338,7 @@ Global
{884C0FA4-16A5-408F-A528-37C7F89C8701} = {E6F1DC30-D346-4E86-8F58-20CFAAB19448}
{3070F200-DDF7-4969-9967-9F33E1729361} = {E6F1DC30-D346-4E86-8F58-20CFAAB19448}
{AA2DA4CA-FE39-431B-84C2-1C99E3B282D8} = {E6F1DC30-D346-4E86-8F58-20CFAAB19448}
{00F7DD98-B3DB-4361-8F06-6F6F2E29A81A} = {84B95569-6BD8-48B4-A942-CBBC9462B6D4}
{C34D4C62-BEBE-4333-8EB8-9CDDEF331E1B} = {E6F1DC30-D346-4E86-8F58-20CFAAB19448}
EndGlobalSection
EndGlobal
2 changes: 2 additions & 0 deletions DotNetLightning.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=HTLC/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=htlcs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=ITLV/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=libmacaroon/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=lnbc/@EntryIndexedValue">True</s:Boolean>

<s:Boolean x:Key="/Default/UserDictionary/Words/=lnbcrt/@EntryIndexedValue">True</s:Boolean>
Expand All @@ -32,6 +33,7 @@
<s:Boolean x:Key="/Default/UserDictionary/Words/=rbad/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=remotekey/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=rresult/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Satisfier/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=satoshi/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Satoshis/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=sats/@EntryIndexedValue">True</s:Boolean>
Expand Down
6 changes: 6 additions & 0 deletions src/DotNetLightning.Core/DotNetLightning.Core.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
<ProjectReference Condition="'$(BouncyCastle)'!='true'" Include="..\NSec\Experimental\NSec.Experimental.csproj" PrivateAssets="all" />
<ProjectReference Include="..\ResultUtils\ResultUtils.fsproj" PrivateAssets="all" />
<ProjectReference Include="..\InternalBech32Encoder\InternalBech32Encoder.csproj" PrivateAssets="all" />
<ProjectReference Include="..\Macaroons\Macaroons.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Include="AssemblyInfo.fs" />
Expand Down Expand Up @@ -74,6 +75,11 @@
<Compile Include="Payment\PaymentEvents.fs" />
<Compile Include="Payment\Amount.fs" />
<Compile Include="Payment\PaymentRequest.fs" />
<Compile Include="Payment\LSAT\Constants.fs" />
<Compile Include="Payment\LSAT\CaveatsExtensions.fs" />
<Compile Include="Payment\LSAT\MacaroonIdentifier.fs" />
<Compile Include="Payment\LSAT\Service.fs" />
<Compile Include="Payment\LSAT\Satisfier.fs" />
<Compile Include="Routing\Graph.fs" />
<Compile Include="Routing\RouterPrimitives.fs" />
<Compile Include="Routing\NetworkStats.fs" />
Expand Down
23 changes: 23 additions & 0 deletions src/DotNetLightning.Core/Payment/LSAT/CaveatsExtensions.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
namespace DotNetLightning.Payment.LSAT

open Macaroons
open System.Runtime.CompilerServices


[<Extension;AbstractClass;Sealed>]
type CaveatsExtensions() =

/// 'Value' is right hand of '=' in caveats, if it does not contain '=', it will return Error
[<Extension>]
static member TryGetValue(caveat: Caveat) =
let s = caveat.ToString().Split('=')
if (s.Length <> 2) then Error(sprintf "invalid caveat for lsat %s" (caveat.ToString())) else
Ok(s.[1].Trim())

/// 'Condition' is left hand of '=' in caveats, if it does not contain '=', it will return Error
[<Extension>]
static member TryGetCondition(caveat: Caveat) =
let s = caveat.ToString().Split('=')
if (s.Length <> 2) then Error(sprintf "invalid caveat for lsat %s" (caveat.ToString())) else
Ok(s.[0].Trim())

5 changes: 5 additions & 0 deletions src/DotNetLightning.Core/Payment/LSAT/Constants.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
namespace DotNetLightning.Payment.LSAT

module Constants =
[<Literal>]
let CAPABILITIES_CONDITION_PREFIX = "_capabilities"
70 changes: 70 additions & 0 deletions src/DotNetLightning.Core/Payment/LSAT/MacaroonIdentifier.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
namespace DotNetLightning.Payment.LSAT
open System
open System.Net
open System.Net.Http.Headers
open DotNetLightning.Utils.Primitives
open NBitcoin
open DotNetLightning.Core.Utils.Extensions
open DotNetLightning.Utils
open Macaroons
open NSec.Cryptography
open ResultUtils
open NBitcoin.Crypto

module private Helpers =
let hex = DataEncoders.HexEncoder()

type MacaroonIdentifierV0 = {
PaymentHash: PaymentHash
TokenId: uint256
}
with
static member Create(p: PaymentHash) =
{
PaymentHash = p
TokenId = RandomUtils.GetUInt256()
}

/// see ref: https://github.com/lightninglabs/LSAT/blob/master/macaroons.md#macaroon-identifier
type MacaroonIdentifier =
| V0 of MacaroonIdentifierV0
| UnknownVersion of version: uint16 * contents: byte[]
with
static member CreateV0 hash =
MacaroonIdentifierV0.Create hash |> V0
static member TryCreateFromBytes(b: byte[]) =
let e = Error(sprintf "Invalid bytes for macaroon identifier %A" b)
if (b.Length < 2) then e else
match UInt16.FromBytesBigEndian(b.[0..1]) with
| 0us ->
if (b.Length <> 2 + 32 + 32) then e else
{
PaymentHash = PaymentHash.PaymentHash(uint256(b.[2..33], false))
TokenId = uint256(b.[34..], false)
}
|> V0
|> Ok
| x ->
UnknownVersion(x, b.[2..])
|> Ok

member this.ToBytes() =
match this with
| V0 i ->
let r = Array.zeroCreate 66
Array.blit (0us.GetBytesBigEndian()) 0 r 0 2
Array.blit (i.PaymentHash.ToBytes(false)) 0 r 2 32
Array.blit (i.TokenId.ToBytes(false)) 0 r 34 32
r
| UnknownVersion (v, bytes) ->
Array.concat (seq { yield v.GetBytesBigEndian(); yield bytes } )

static member TryParse(str: string) =
str |> Helpers.hex.DecodeData |> MacaroonIdentifier.TryCreateFromBytes
member this.ToHex() =
this.ToBytes() |> Helpers.hex.EncodeData

member this.Hash =
this.ToBytes() |> Hashes.SHA256 |> fun x -> uint256(x, false)
override this.ToString() = this.ToHex()

133 changes: 133 additions & 0 deletions src/DotNetLightning.Core/Payment/LSAT/Satisfier.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
namespace DotNetLightning.Payment.LSAT

open ResultUtils
open DotNetLightning.Utils
open Macaroons
open System.Collections.Generic
open System.Runtime.CompilerServices
open NBitcoin

/// When we verify a macaroon for its caveats, usually it check each caveats independently.
/// In case of LSAT, this does not work since the validity of a caveat depends on a previous caveat
/// (more specifically, if there were two caveats with a same Condition, we usually check that whether the restriction
/// does not get more loose than before.)
/// So we can not just rely on Macaroon's <see cref="Verifier"/> . The additional check is done with this ISatisfier
/// Interface
type ISatisfier =
/// This is the left side of caveat equation. e.g. for caveat "service=my_awesome_service", it is "service"
abstract Condition: string
/// ensures a caveat is in accordance with a previous one with the same condition. This is needed since caveats
/// of the same condition can be used multiple times as long as they enforce more permissions than the previous.
///
/// For example, we have a caveat that only allows us to use an LSAT for 7 more days. we can add another caveat
/// that only allows for 3 more days of use and lend it to another party.
abstract SatisfyPrevious: Caveat * Caveat -> Result<unit, string>
/// Satisfies the final caveat of an LSAT. If multiple caveats with the same condition exist, this will only
/// be executed once all previous caveats are also satisfied.
abstract SatisfyFinal: Caveat -> Result<unit, string>

type ServiceSatisfier(targetService: string) =
do
checkNull "targetService" targetService

interface ISatisfier with
member this.Condition = "service"
member this.SatisfyPrevious(prev, curr) =
result {
let! prevServiceValue = prev.TryGetValue()
let! prevServices = Service.ParseMany(prevServiceValue.ToString())
let prevServices = prevServices |> HashSet

let! currentServiceValue = curr.TryGetValue()
let! currentServices = Service.ParseMany(currentServiceValue.ToString())
for s in currentServices do
if not <| prevServices.Contains(s) then
return! Error(sprintf "Service (%s) was not previously allowed!" s.Name)
else
()
}
member this.SatisfyFinal(finalCaveat) =
result {
let! serviceValue = finalCaveat.TryGetValue()
let! services = Service.ParseMany(serviceValue.ToString())
if services |> Seq.exists(fun s -> s.Name = targetService) then
return ()
else
return! Error(sprintf "Target service %s not found" targetService)
}

type CapabilitiesSatisfier(service: string, targetCapability: string) =
do
checkNull "service" service
checkNull "targetCapability" targetCapability
member val Condition = sprintf "%s%s" service Constants.CAPABILITIES_CONDITION_PREFIX with get

interface ISatisfier with
member this.Condition = this.Condition
member this.SatisfyPrevious(prev, curr) =
result {
let! prevValue = prev.TryGetValue()
let prevCapabilities = prevValue.Split ',' |> HashSet
let! currentValue = curr.TryGetValue()
let currentCapabilities = currentValue.Split ','
for c in currentCapabilities do
if (not <| prevCapabilities.Contains(c)) then
return! Error(sprintf "Capability (%A) was not previously allowed!" c)
()
}
member this.SatisfyFinal(finalCaveat) =
result {
let! caps = finalCaveat.TryGetValue()
let caps = caps.Split ','
if (caps |> Seq.exists((=)targetCapability)) then
return ()
else return! Error(sprintf "target capability (%A) not found" targetCapability)
}

[<Extension;AbstractClass;Sealed>]
type MacaroonExtensions() =
/// Verifies that the macaroon is compliant with LSAT.
[<Extension>]
static member VerifyLSATCaveats(macaroon: Macaroon, satisfiers: IList<ISatisfier>, secret: string): VerificationResult =
macaroon.VerifyLSATCaveats(macaroon.Caveats, satisfiers, secret)

/// Verifies that the macaroon is compliant with LSAT.
[<Extension>]
static member VerifyLSATCaveats(macaroon: Macaroon, caveats: IList<Caveat>, satisfiers: IList<ISatisfier>, secret: string): VerificationResult =
result {
let caveatSatisfiers = Dictionary()
for s in satisfiers do
caveatSatisfiers.AddOrReplace(s.Condition, s)

let relevantCaveats = Dictionary<string, ResizeArray<Caveat>>()
for c in caveats do
let! condition = c.TryGetCondition()
if (caveatSatisfiers.ContainsKey(condition)) then
match relevantCaveats.TryGetValue(condition) with
| true, caveatsSoFar ->
caveatsSoFar.Add(c)
relevantCaveats.AddOrReplace(condition, caveatsSoFar)
| false, _ ->
let cs = ResizeArray()
cs.Add(c)
relevantCaveats.Add(condition, cs)

for kv in relevantCaveats do
let condition, caveats = kv.Key, kv.Value
let s = caveatSatisfiers.[condition]
for i in 0..(caveats.Count - 2) do
let prev = caveats.[i]
let curr = caveats.[i + 1]
do! s.SatisfyPrevious(prev, curr)
do! s.SatisfyFinal(caveats |> Seq.last)
}
|>
function
| Ok _ ->
// finally check each caveats independently
let v = Verifier()
for c in caveats do
v.SatisfyExact(c.CId)
macaroon.Verify(v, secret)
| Error e ->
VerificationResult(e)
53 changes: 53 additions & 0 deletions src/DotNetLightning.Core/Payment/LSAT/Service.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
namespace DotNetLightning.Payment.LSAT

open System
open System.Collections.Generic

open Macaroons
open ResultUtils

open System.Text
open DotNetLightning.Utils

/// See: https://github.com/lightninglabs/LSAT/blob/master/macaroons.md#target-services
type Service = {
Name: string
ServiceTier: uint8
Price: LNMoney option
}
with
static member Create(name, tier) =
{
Name = name
ServiceTier = tier
Price = None
}
static member ParseMany(s: string) =
if (String.IsNullOrEmpty(s)) then Error("empty service") else
let rawServices = s.Split ','
result {
let mutable res = ResizeArray()
for rawService in rawServices do
let serviceInfo = rawService.Split ':'
if serviceInfo.Length <> 2 then return! Error(sprintf "Invalid value for service %A" serviceInfo) else
let (name, tier) = serviceInfo.[0], serviceInfo.[1]
let! t =
match Byte.TryParse tier with
| true, t -> Ok t
| false, _ -> Error(sprintf "invalid service caveat value %s Service tier must be uint8 (%s)" s tier)
res.Add(Service.Create(name, t))
return res
}

static member EncodeToCaveat(services: IList<Service>): Result<Caveat, _> =
result {
if (services.Count = 0) then return! Error("empty service") else
if (String.IsNullOrEmpty services.[0].Name) then return! Error ("Missing service name") else
let sb = StringBuilder()
sb.Append("services=") |> ignore
sb.Append(sprintf "%s:%d" services.[0].Name services.[0].ServiceTier) |> ignore
for s in services do
if (String.IsNullOrEmpty s.Name) then return! Error ("Missing service name") else
sb.Append(sprintf ",%s:%d" s.Name s.ServiceTier) |> ignore
return sb.ToString() |> Caveat
}
3 changes: 2 additions & 1 deletion src/DotNetLightning.Core/Payment/PaymentRequest.fs
Original file line number Diff line number Diff line change
Expand Up @@ -514,12 +514,13 @@ type private Bolt11Data = {
|> Helpers.convert5BitsTo8


/// a.k.a bolt11-invoice, lightning-invoice
type PaymentRequest = private {
Prefix: string
Amount: LNMoney option
Timestamp: DateTimeOffset
NodeId: NodeId
Tags:TaggedFields
Tags: TaggedFields
Signature: (LNECDSASignature * byte) option
}
with
Expand Down
Loading

0 comments on commit 176587e

Please sign in to comment.