diff --git a/NOnion.Tests/CircuitHelper.cs b/NOnion.Tests/CircuitHelper.cs index a76de0bb..251eda3f 100644 --- a/NOnion.Tests/CircuitHelper.cs +++ b/NOnion.Tests/CircuitHelper.cs @@ -23,7 +23,9 @@ static private CircuitNodeDetail ConvertToCircuitNodeDetail(ServerDescriptorEntr var fingerprintBytes = Hex.ToByteArray(server.Fingerprint.Value); var nTorOnionKeyBytes = Base64Util.FromString(server.NTorOnionKey.Value); var endpoint = IPEndPoint.Parse($"{server.Address.Value}:{server.OnionRouterPort.Value}"); - return CircuitNodeDetail.NewCreate(endpoint, nTorOnionKeyBytes, fingerprintBytes); + return CircuitNodeDetail.NewCreate(endpoint, + new NTorOnionKey(nTorOnionKeyBytes), + new Fingerprint(fingerprintBytes)); } /* It's possible that the router returned by GetRandomFallbackDirectory diff --git a/NOnion.Tests/KeyBlindingTests.cs b/NOnion.Tests/KeyBlindingTests.cs index a5e2d6f0..bade62db 100644 --- a/NOnion.Tests/KeyBlindingTests.cs +++ b/NOnion.Tests/KeyBlindingTests.cs @@ -17,7 +17,7 @@ public class KeyBlindingTests [Test] public void CheckBlindedPublicKey() { - var computed = HiddenServicesCipher.CalculateBlindedPublicKey(blindingFactor, publicKey); + var computed = HiddenServicesCipher.CalculateBlindedPublicKey(blindingFactor, publicKey).ToByteArray(); CollectionAssert.AreEqual(computed, blindedPublicKey); } } diff --git a/NOnion.Tests/TorDirectoryTests.cs b/NOnion.Tests/TorDirectoryTests.cs index 0285b663..8be3c1ad 100644 --- a/NOnion.Tests/TorDirectoryTests.cs +++ b/NOnion.Tests/TorDirectoryTests.cs @@ -56,8 +56,8 @@ private async Task ReturnRandomRouter() TorDirectory directory = await TorDirectory.BootstrapAsync(FallbackDirectorySelector.GetRandomFallbackDirectory(), cachePath); var (endPoint, router) = await directory.GetRouterAsync(RouterType.Normal); Assert.IsTrue(router.IsCreate); - Assert.IsFalse(((CircuitNodeDetail.Create)router).IdentityKey.All(x => x == 0)); - Assert.IsFalse(((CircuitNodeDetail.Create)router).NTorOnionKey.All(x => x == 0)); + Assert.IsFalse(((CircuitNodeDetail.Create)router).Fingerprint.ToByteArray().All(x => x == 0)); + Assert.IsFalse(((CircuitNodeDetail.Create)router).NTorOnionKey.ToByteArray().All(x => x == 0)); Assert.IsNotNull(((CircuitNodeDetail.Create)router).EndPoint); Assert.That(endPoint, Is.EqualTo(((CircuitNodeDetail.Create)router).EndPoint)); } diff --git a/NOnion/Cells/Relay/RelayIntroduce.fs b/NOnion/Cells/Relay/RelayIntroduce.fs index d45f3ada..fb3c93f4 100644 --- a/NOnion/Cells/Relay/RelayIntroduce.fs +++ b/NOnion/Cells/Relay/RelayIntroduce.fs @@ -10,7 +10,7 @@ type RelayIntroduceInnerData = { RendezvousCookie: array Extensions: List - OnionKey: array + OnionKey: NTorOnionKey RendezvousLinkSpecifiers: List } @@ -41,7 +41,8 @@ type RelayIntroduceInnerData = if onionKeyLength <> Constants.NTorPublicKeyLength then failwith "Invalid onion key length" - let onionKey = reader.ReadBytes Constants.NTorPublicKeyLength + let onionKey = + reader.ReadBytes Constants.NTorPublicKeyLength |> NTorOnionKey let rec readLinkSpecifier (n: byte) (state: List) = if n = 0uy then @@ -62,6 +63,8 @@ type RelayIntroduceInnerData = } member self.ToBytes() = + let onionKeyBytes = self.OnionKey.ToByteArray() + Array.concat [ self.RendezvousCookie @@ -70,10 +73,10 @@ type RelayIntroduceInnerData = |> List.map(fun ext -> ext.ToBytes()) |> Array.concat Array.singleton 1uy - self.OnionKey.Length + onionKeyBytes.Length |> uint16 |> IntegerSerialization.FromUInt16ToBigEndianByteArray - self.OnionKey + onionKeyBytes self.RendezvousLinkSpecifiers.Length |> byte |> Array.singleton self.RendezvousLinkSpecifiers |> List.map(fun link -> link.ToBytes()) diff --git a/NOnion/Constants.fs b/NOnion/Constants.fs index 17294f9c..88ee484b 100644 --- a/NOnion/Constants.fs +++ b/NOnion/Constants.fs @@ -31,6 +31,15 @@ module Constants = [] let KdfLength = 92 + [] + let FingerprintLength = 20 + + [] + let Ed25519PrivateKeyLength = 32 + + [] + let ExpandedEd25519PrivateKeyLength = 64 + let internal SupportedProtocolVersion: array = [| 3us |] (* diff --git a/NOnion/Crypto/HiddenServicesCipher.fs b/NOnion/Crypto/HiddenServicesCipher.fs index 202c61bb..083468b6 100644 --- a/NOnion/Crypto/HiddenServicesCipher.fs +++ b/NOnion/Crypto/HiddenServicesCipher.fs @@ -88,14 +88,15 @@ module HiddenServicesCipher = Ed25519Clamp blindingFactor match Ed25519.CalculateBlindedPublicKey(publicKey, blindingFactor) with - | true, output -> output + | true, output -> ED25519PublicKey.FromBytes output | false, _ -> failwith "can't calculate blinded public key" let CalculateExpandedBlindedPrivateKey (blindingFactor: array) (masterPrivateKey: array) - = - let expandedMasterPrivateKey = Array.zeroCreate 64 + : ExpandedEd25519PrivateKey = + let expandedMasterPrivateKey = + Array.zeroCreate Constants.ExpandedEd25519PrivateKeyLength let hashEngine = Sha512Digest() hashEngine.BlockUpdate(masterPrivateKey, 0, masterPrivateKey.Length) @@ -112,14 +113,14 @@ module HiddenServicesCipher = "Derive temporary signing key hash input" ) with - | true, output -> output + | true, output -> ExpandedEd25519PrivateKey output | false, _ -> failwith "can't calculate blinded private key" let BuildBlindedPublicKey (periodNumber: uint64, periodLength: uint64) (publicKey: array) - = + : ED25519PublicKey = let blindingFactor = CalculateBlindingFactor periodNumber periodLength publicKey @@ -154,7 +155,7 @@ module HiddenServicesCipher = [ "subcredential" |> Encoding.ASCII.GetBytes credential - blindedKey + blindedKey.ToByteArray() ] |> SHA3256 diff --git a/NOnion/Directory/HiddenServiceDescriptorDocument.fs b/NOnion/Directory/HiddenServiceDescriptorDocument.fs index c5805595..60da092b 100644 --- a/NOnion/Directory/HiddenServiceDescriptorDocument.fs +++ b/NOnion/Directory/HiddenServiceDescriptorDocument.fs @@ -8,7 +8,7 @@ open NOnion.Utility type IntroductionPointEntry = { - OnionKey: Option> + OnionKey: Option AuthKey: Option EncKey: Option> EncKeyCert: Option @@ -60,7 +60,10 @@ type IntroductionPointEntry = innerParse { state with OnionKey = - readWord() |> Convert.FromBase64String |> Some + readWord() + |> Convert.FromBase64String + |> NTorOnionKey + |> Some } | "enc-key" -> lines.Dequeue() |> ignore @@ -127,9 +130,11 @@ type IntroductionPointEntry = "HS document (most inner wrapper) is incomplete, missing linkspecifier" match self.OnionKey with - | Some onionKey -> + | Some nTorOnionKey -> appendLine( - sprintf "onion-key ntor %s" (Convert.ToBase64String onionKey) + sprintf + "onion-key ntor %s" + (Convert.ToBase64String <| nTorOnionKey.ToByteArray()) ) |> ignore | None -> diff --git a/NOnion/Directory/TorDirectory.fs b/NOnion/Directory/TorDirectory.fs index 7e03a77a..7edd56dd 100644 --- a/NOnion/Directory/TorDirectory.fs +++ b/NOnion/Directory/TorDirectory.fs @@ -276,8 +276,8 @@ type TorDirectory = endpoint, CircuitNodeDetail.Create( endpoint, - nTorOnionKeyBytes, - fingerprintBytes + NTorOnionKey nTorOnionKeyBytes, + Fingerprint fingerprintBytes ) } @@ -583,7 +583,7 @@ type TorDirectory = |> Async.StartAsTask member self.GetResponsibleHiddenServiceDirectories - (blindedPublicKey: array) + (ED25519PublicKey blindedPublicKey) (sharedRandomValue: string) (periodNumber: uint64) (periodLength: uint64) @@ -654,7 +654,7 @@ type TorDirectory = Array.concat [ "store-at-idx" |> Encoding.ASCII.GetBytes - blindedPublicKey + blindedPublicKey.GetEncoded() replicaNum |> uint64 |> IntegerSerialization.FromUInt64ToBigEndianByteArray diff --git a/NOnion/KeyTypes.fs b/NOnion/KeyTypes.fs new file mode 100644 index 00000000..32244ba0 --- /dev/null +++ b/NOnion/KeyTypes.fs @@ -0,0 +1,70 @@ +namespace NOnion + +open Org.BouncyCastle.Crypto.Parameters + + +type ExpandedEd25519PrivateKey(bytes: array) = + do + if bytes.Length <> Constants.ExpandedEd25519PrivateKeyLength then + failwithf + "Invalid private key (length=%d), %d expected" + bytes.Length + Constants.ExpandedEd25519PrivateKeyLength + + member self.ToByteArray() = + bytes + +[] +type Ed25519PrivateKey = + | Normal of Ed25519PrivateKeyParameters + | Expanded of ExpandedEd25519PrivateKey + + static member FromBytes(bytes: array) : Ed25519PrivateKey = + if bytes.Length = Constants.ExpandedEd25519PrivateKeyLength then + Expanded <| ExpandedEd25519PrivateKey bytes + elif bytes.Length = Constants.Ed25519PrivateKeyLength then + Normal <| Ed25519PrivateKeyParameters(bytes, 0) + else + failwithf + "Invalid private key (length=%d), private key should either be %d (standard ed25519) or %d bytes (expanded ed25519 key)" + bytes.Length + Constants.Ed25519PrivateKeyLength + Constants.ExpandedEd25519PrivateKeyLength + + member self.ToByteArray() = + match self with + | Normal key -> key.GetEncoded() + | Expanded key -> key.ToByteArray() + +type ED25519PublicKey = + | ED25519PublicKey of Ed25519PublicKeyParameters + + static member FromBytes(bytes: array) : ED25519PublicKey = + ED25519PublicKey <| Ed25519PublicKeyParameters(bytes, 0) + + member self.ToByteArray() = + match self with + | ED25519PublicKey publicKeyParams -> publicKeyParams.GetEncoded() + +type NTorOnionKey(bytes: array) = + do + if bytes.Length <> Constants.NTorPublicKeyLength then + failwithf + "Invalid onion key length, expected %d, got %d" + Constants.NTorPublicKeyLength + bytes.Length + + member self.ToByteArray() = + bytes + +/// Digest of identity key. +type Fingerprint(bytes: array) = + do + if bytes.Length <> Constants.FingerprintLength then + failwithf + "Invalid fingerprint (identity key digest) length, expected %d, got %d" + Constants.FingerprintLength + bytes.Length + + member self.ToByteArray() = + bytes diff --git a/NOnion/NOnion.fsproj b/NOnion/NOnion.fsproj index 50428413..9d4ebae3 100644 --- a/NOnion/NOnion.fsproj +++ b/NOnion/NOnion.fsproj @@ -19,6 +19,7 @@ + diff --git a/NOnion/Network/TorCircuit.fs b/NOnion/Network/TorCircuit.fs index 39edec50..6e4672d7 100644 --- a/NOnion/Network/TorCircuit.fs +++ b/NOnion/Network/TorCircuit.fs @@ -25,8 +25,8 @@ type CircuitNodeDetail = | FastCreate | Create of EndPoint: IPEndPoint * - NTorOnionKey: array * - IdentityKey: array + NTorOnionKey: NTorOnionKey * + Fingerprint: Fingerprint member self.GetIdentityKey() = match self with @@ -694,7 +694,7 @@ and TorCircuit { LinkSpecifier.Type = LinkSpecifierType.LegacyIdentity - Data = identityKey + Data = identityKey.ToByteArray() } ] HandshakeType = HandshakeType.NTor diff --git a/NOnion/Network/TorGuard.fs b/NOnion/Network/TorGuard.fs index 8450be42..0c215daf 100644 --- a/NOnion/Network/TorGuard.fs +++ b/NOnion/Network/TorGuard.fs @@ -49,7 +49,7 @@ type TorGuard ( client: TcpClient, sslStream: SslStream, - fingerprintOpt: Option> + fingerprintOpt: Option ) = let shutdownToken = new CancellationTokenSource() @@ -116,7 +116,7 @@ type TorGuard static member private InnerNewClient (ipEndpoint: IPEndPoint) - (fingerprintOpt: Option>) + (fingerprintOpt: Option) = async { let tcpClient = new TcpClient() @@ -475,7 +475,7 @@ type TorGuard let sha1 = SHA1.Create() let hash = sha1.ComputeHash(getPublicKeyBytes idCertificate) - if hash <> fingerprint then + if hash <> fingerprint.ToByteArray() then raise <| GuardConnectionFailedException("RSA Identity was invalid") | None -> () diff --git a/NOnion/Services/TorServiceClient.fs b/NOnion/Services/TorServiceClient.fs index 9264dfbc..ef626bcf 100644 --- a/NOnion/Services/TorServiceClient.fs +++ b/NOnion/Services/TorServiceClient.fs @@ -110,8 +110,9 @@ type TorServiceClient = (sprintf "/tor/hs/%i/%s" Constants.HiddenServices.Version - ((Convert.ToBase64String - blindedPublicKey))) + (Convert.ToBase64String + <| blindedPublicKey.ToByteArray + ())) false return @@ -188,7 +189,7 @@ type TorServiceClient = let secretInput = Array.concat [ - blindedPublicKey + blindedPublicKey.ToByteArray() HiddenServicesCipher.GetSubCredential (periodNum, periodLength) publicKey @@ -372,7 +373,7 @@ type TorServiceClient = CircuitNodeDetail.Create( endpointSpecifier, introductionPoint.OnionKey.Value, - identityKey + Fingerprint identityKey ) return @@ -433,7 +434,7 @@ type TorServiceClient = { LinkSpecifier.Type = LinkSpecifierType.LegacyIdentity - Data = identityKey + Data = identityKey.ToByteArray() } ] } diff --git a/NOnion/Services/TorServiceHost.fs b/NOnion/Services/TorServiceHost.fs index 7d6c10e0..c66b9085 100644 --- a/NOnion/Services/TorServiceHost.fs +++ b/NOnion/Services/TorServiceHost.fs @@ -29,9 +29,9 @@ type IntroductionPointInfo = Address: IPEndPoint EncryptionKey: AsymmetricCipherKeyPair AuthKey: AsymmetricCipherKeyPair - MasterPublicKey: array - OnionKey: array - Fingerprint: array + MasterPublicKey: Ed25519PublicKeyParameters + NTorOnionKey: NTorOnionKey + Fingerprint: Fingerprint } type TorServiceHost @@ -195,7 +195,7 @@ type TorServiceHost (introductionPointDetails.EncryptionKey.Public :?> X25519PublicKeyParameters) periodInfo - introductionPointDetails.MasterPublicKey + (introductionPointDetails.MasterPublicKey.GetEncoded()) use decryptedStream = new MemoryStream(decryptedData) use decryptedReader = new BinaryReader(decryptedStream) @@ -234,7 +234,7 @@ type TorServiceHost |> Seq.tryExactlyOne match linkSpecifierOpt with - | Some linkSpecifier -> linkSpecifier.Data + | Some linkSpecifier -> Fingerprint linkSpecifier.Data | None -> failwith "No rendezvous fingerprint found!" let connectToRendezvousJob = @@ -305,10 +305,9 @@ type TorServiceHost IntroductionPointInfo.Address = address AuthKey = authKeyPair EncryptionKey = encKeyPair - OnionKey = onionKey + NTorOnionKey = onionKey Fingerprint = fingerprint - MasterPublicKey = - masterPublicKey.GetEncoded() + MasterPublicKey = masterPublicKey } do! circuit.Create guardNodeDetail |> Async.Ignore @@ -483,7 +482,7 @@ type TorServiceHost let secretInput = Array.concat [ - blindedPublicKey + blindedPublicKey.ToByteArray() HiddenServicesCipher.GetSubCredential (periodNum, periodLength) (masterPublicKey.GetEncoded()) @@ -507,7 +506,9 @@ type TorServiceHost { LinkSpecifier.Type = LinkSpecifierType.LegacyIdentity - Data = info.Fingerprint + Data = + info.Fingerprint.ToByteArray + () } ] @@ -529,9 +530,10 @@ type TorServiceHost ((info.AuthKey.Public :?> Ed25519PublicKeyParameters) .GetEncoded()) - (descriptorSigningPublicKey.GetEncoded()) - (descriptorSigningPrivateKey.GetEncoded - ()) + (descriptorSigningPublicKey + |> ED25519PublicKey) + (descriptorSigningPrivateKey + |> Ed25519PrivateKey.Normal) Constants.HiddenServices.Descriptor.CertificateLifetime let encKeyBytes = @@ -556,14 +558,15 @@ type TorServiceHost Certificate.CreateNew CertType.IntroPointEncKeySignedByDescriptorSigningKey convertedX25519Key - (descriptorSigningPublicKey.GetEncoded()) - (descriptorSigningPrivateKey.GetEncoded - ()) + (descriptorSigningPublicKey + |> ED25519PublicKey) + (descriptorSigningPrivateKey + |> Ed25519PrivateKey.Normal) Constants.HiddenServices.Descriptor.CertificateLifetime { IntroductionPointEntry.OnionKey = - Some info.OnionKey + Some info.NTorOnionKey AuthKey = Some authKeyCert EncKey = Some encKeyBytes EncKeyCert = Some encKeyCert @@ -685,7 +688,7 @@ type TorServiceHost CertType.ShortTermDescriptorSigningKeyByBlindedPublicKey (descriptorSigningPublicKey.GetEncoded()) blindedPublicKey - blindedPrivateKey + (Ed25519PrivateKey.Expanded blindedPrivateKey) Constants.HiddenServices.Descriptor.CertificateLifetime HiddenServiceFirstLayerDescriptorDocument.CreateNew diff --git a/NOnion/TorHandshakes/NTorHandshake.fs b/NOnion/TorHandshakes/NTorHandshake.fs index 9f88a43a..501fd322 100644 --- a/NOnion/TorHandshakes/NTorHandshake.fs +++ b/NOnion/TorHandshakes/NTorHandshake.fs @@ -15,14 +15,14 @@ type NTorHandshake = { RandomClientPrivateKey: X25519PrivateKeyParameters RandomClientPublicKey: X25519PublicKeyParameters - IdentityDigest: array + IdentityDigest: Fingerprint NTorOnionKey: X25519PublicKeyParameters } static member Create - (identityDigest: array) - (nTorOnionKey: array) + (identityDigest: Fingerprint) + (nTorOnionKey: NTorOnionKey) = let privateKey, publicKey = @@ -37,7 +37,8 @@ type NTorHandshake = { IdentityDigest = identityDigest - NTorOnionKey = X25519PublicKeyParameters(nTorOnionKey, 0) + NTorOnionKey = + X25519PublicKeyParameters(nTorOnionKey.ToByteArray(), 0) RandomClientPrivateKey = privateKey RandomClientPublicKey = publicKey } @@ -46,7 +47,7 @@ type NTorHandshake = member self.GenerateClientMaterial() = Array.concat [ - self.IdentityDigest + self.IdentityDigest.ToByteArray() self.NTorOnionKey.GetEncoded() self.RandomClientPublicKey.GetEncoded() ] @@ -79,7 +80,7 @@ type NTorHandshake = [ sharedSecretWithY sharedSecretWithB - self.IdentityDigest + self.IdentityDigest.ToByteArray() self.NTorOnionKey.GetEncoded() self.RandomClientPublicKey.GetEncoded() randomServerPublicKey.GetEncoded() @@ -97,7 +98,7 @@ type NTorHandshake = Array.concat [ verify - self.IdentityDigest + self.IdentityDigest.ToByteArray() self.NTorOnionKey.GetEncoded() randomServerPublicKey.GetEncoded() self.RandomClientPublicKey.GetEncoded() diff --git a/NOnion/Utility/CertificateUtil.fs b/NOnion/Utility/CertificateUtil.fs index b4327b88..901665cd 100644 --- a/NOnion/Utility/CertificateUtil.fs +++ b/NOnion/Utility/CertificateUtil.fs @@ -70,8 +70,8 @@ type Certificate = static member CreateNew certType (certifiedKey: array) - (signingPublicKey: array) - (signingPrivateKey: array) + (ED25519PublicKey signingPublicKey) + (signingPrivateKey: Ed25519PrivateKey) (lifetime: TimeSpan) = let unsignedCertificate = @@ -92,7 +92,7 @@ type Certificate = CertificateExtension.Type = CertificateExtensionType.SignedWithEd25519Key Flags = 0uy - Data = signingPublicKey + Data = signingPublicKey.GetEncoded() } ) Signature = Array.empty @@ -101,13 +101,14 @@ type Certificate = let unsignedCertificateBytes = unsignedCertificate.ToBytes true let signature = - if signingPrivateKey.Length = 32 then + match signingPrivateKey with + | Ed25519PrivateKey.Normal privateKey -> //Standard private key, we can sign with bouncycastle let signer = Ed25519Signer() signer.Init( true, - Ed25519PrivateKeyParameters(signingPrivateKey, 0) + Ed25519PrivateKeyParameters(privateKey.GetEncoded(), 0) ) signer.BlockUpdate( @@ -117,21 +118,18 @@ type Certificate = ) signer.GenerateSignature() - elif signingPrivateKey.Length = 64 then + | Ed25519PrivateKey.Expanded privateKey -> //Expanded private key, we have to sign with Chaos.NaCl let signature = Array.zeroCreate 64 Ed25519.SignWithPrehashedPrivateKey( ArraySegment signature, ArraySegment unsignedCertificateBytes, - ArraySegment signingPrivateKey, - ArraySegment signingPublicKey + ArraySegment(privateKey.ToByteArray()), + ArraySegment(signingPublicKey.GetEncoded()) ) signature - else - failwith - "Invalid private key, private key should either be 32 (standard ed25519) or 64 bytes (expanded ed25519 key)" { unsignedCertificate with Signature = signature