forked from joemphilips/DotNetLightning
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request joemphilips#101 from joemphilips/macaroon
add package for macaroon and its test
- Loading branch information
Showing
44 changed files
with
2,581 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
23 changes: 23 additions & 0 deletions
23
src/DotNetLightning.Core/Payment/LSAT/CaveatsExtensions.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
70
src/DotNetLightning.Core/Payment/LSAT/MacaroonIdentifier.fs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.