/// <summary> /// Asynchronously writes the pair record for a device. /// </summary> /// <param name="udid"> /// The UDID of the device for which to save the pair record. /// </param> /// <param name="pairingRecord"> /// The pairing record for the device. /// </param> /// <param name="cancellationToken"> /// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation. /// </param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public async virtual Task SavePairingRecordAsync(string udid, PairingRecord pairingRecord, CancellationToken cancellationToken) { if (udid == null) { throw new ArgumentNullException(nameof(udid)); } if (pairingRecord == null) { throw new ArgumentNullException(nameof(pairingRecord)); } await using (var protocol = await this.TryConnectToMuxerAsync(cancellationToken).ConfigureAwait(false)) { // Send the save ReadPairRecord message await protocol.WriteMessageAsync( new SavePairingRecordMessage() { MessageType = MuxerMessageType.SavePairRecord, PairRecordID = udid, PairRecordData = pairingRecord.ToByteArray(), }, cancellationToken).ConfigureAwait(false); var response = (ResultMessage)await protocol.ReadMessageAsync(cancellationToken).ConfigureAwait(false); if (response.Number != MuxerError.Success) { throw new MuxerException($"An error occurred while saving the pairing record for device {udid}: {response.Number}.", response.Number); } } }
public void CopyWithPrivateKeyForSsl_ThrowsOnNull() { var pairingRecord = PairingRecord.Read(File.ReadAllBytes("Lockdown/0123456789abcdef0123456789abcdef01234567.plist")); Assert.Throws <ArgumentNullException>(() => X509Certificate2Extensions.CopyWithPrivateKeyForSsl(pairingRecord.HostCertificate, null)); Assert.Throws <ArgumentNullException>(() => X509Certificate2Extensions.CopyWithPrivateKeyForSsl(null, pairingRecord.HostPrivateKey)); }
/// <summary> /// Asynchronously reads the pair record for a device. /// </summary> /// <param name="udid"> /// The UDID of the device for which to read the pair record. /// </param> /// <param name="cancellationToken"> /// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation. /// </param> /// <returns> /// A <see cref="Task"/> which represents the asynchronous operation, and which returns the pairing /// record. /// </returns> public async virtual Task <PairingRecord> ReadPairingRecordAsync(string udid, CancellationToken cancellationToken) { if (udid == null) { throw new ArgumentNullException(nameof(udid)); } await using (var protocol = await this.TryConnectToMuxerAsync(cancellationToken).ConfigureAwait(false)) { // Send the read ReadPairRecord message await protocol.WriteMessageAsync( new ReadPairingRecordMessage() { MessageType = MuxerMessageType.ReadPairRecord, PairRecordID = udid, }, cancellationToken).ConfigureAwait(false); var response = (ResultMessage)await protocol.ReadMessageAsync(cancellationToken).ConfigureAwait(false); if (response.Number == MuxerError.BadDevice) { return(null); } else if (response.Number != MuxerError.Success) { throw new MuxerException($"An error occurred while reading the pairing record for device {udid}: {response.Number}.", response.Number); } var pairingRecordResponse = (PairingRecordDataMessage)response; var pairingRecord = PairingRecord.Read(pairingRecordResponse.PairRecordData); return(pairingRecord); } }
public void ToString_ValidPairingRecord() { var raw = File.ReadAllText("Lockdown/0123456789abcdef0123456789abcdef01234567.plist"); var dict = (NSDictionary)XmlPropertyListParser.ParseString(raw); var pairingRecord = PairingRecord.Read(dict); Assert.Equal("HostId: 01234567-012345678901234567, SystemBUID: 01234567890123456789012345, Host certificate: EE63391AA1FBA937E2784CC7DAAA9C22BA223B54 (expires: 2026-11-28 11:20:17Z)", pairingRecord.ToString()); }
public void ToPropertyList_ExcludesPrivateKeysIfRequired() { var raw = File.ReadAllText("Lockdown/0123456789abcdef0123456789abcdef01234567.plist"); var dict = (NSDictionary)XmlPropertyListParser.ParseString(raw); var pairingRecord = PairingRecord.Read(dict); var serializedDict = pairingRecord.ToPropertyList(includePrivateKeys: false); Assert.False(serializedDict.ContainsKey("HostPrivateKey")); Assert.False(serializedDict.ContainsKey("RootPrivateKey")); }
public void ToPropertyList_NoPrivateKeys_Works() { var raw = File.ReadAllText("Lockdown/0123456789abcdef0123456789abcdef01234567.plist"); var dict = (NSDictionary)XmlPropertyListParser.ParseString(raw); var pairingRecord = PairingRecord.Read(dict); pairingRecord.HostPrivateKey = null; pairingRecord.RootPrivateKey = null; var serializedDict = pairingRecord.ToPropertyList(); Assert.Equal(dict.Keys.Count - 2, serializedDict.Keys.Count); }
public async Task WriteAsync_Works_Async() { var record = new PairingRecord(); var muxerClient = new Mock <MuxerClient>(MockBehavior.Strict); muxerClient.Setup(c => c.SavePairingRecordAsync("abc", record, default)).Returns(Task.FromResult(record)); var store = new MuxerPairingRecordStore(muxerClient.Object, NullLogger <MuxerPairingRecordStore> .Instance); await store.WriteAsync("abc", record, default).ConfigureAwait(false); muxerClient.Verify(); }
/// <summary> /// Asynchronously enables SSL communications with the device. /// </summary> /// <param name="pairingRecord"> /// A <see cref="PairingRecord"/> which contains the certificates used to authenticate the host and the device. /// </param> /// <param name="cancellationToken"> /// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation. /// </param> /// <returns> /// A <see cref="Task"/> representing the asynchronous operation. /// </returns> public virtual async Task EnableSslAsync(PairingRecord pairingRecord, CancellationToken cancellationToken) { Verify.NotDisposed(this); if (pairingRecord == null) { throw new ArgumentNullException(nameof(pairingRecord)); } if (this.stream is SslStream) { throw new InvalidOperationException("This connection is already using SSL"); } // This block of code constructs a TLS ("SSL") stream which enables you to connect with lockdown over an encrypted // connection. It uses the built-in SslStream class. // AllowNoEncryption will use OpenSSL security policy/level 0, see // https://github.com/dotnet/runtime/blob/master/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.OpenSsl.cs // https://github.com/dotnet/runtime/blob/master/src/libraries/Native/Unix/System.Security.Cryptography.Native/pal_ssl.c // When using security policy/level 1, the root CA may be rejected on Ubuntu 20.04 (which uses OpenSSL 1.1) var encryptionPolicy = EncryptionPolicy.AllowNoEncryption; var sslStream = new SslStream( innerStream: this.stream, leaveInnerStreamOpen: true, userCertificateSelectionCallback: (object sender, string targetHost, X509CertificateCollection localCertificates, X509Certificate remoteCertificate, string[] acceptableIssuers) => { return(localCertificates[0]); }, userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) => { var expectedDeviceCertHash = pairingRecord.DeviceCertificate.GetCertHashString(); var actualDeviceCertHash = certificate.GetCertHashString(); return(string.Equals(expectedDeviceCertHash, actualDeviceCertHash, StringComparison.OrdinalIgnoreCase)); }, encryptionPolicy: encryptionPolicy); X509CertificateCollection clientCertificates = new X509CertificateCollection(); clientCertificates.Add(pairingRecord.HostCertificate.CopyWithPrivateKeyForSsl(pairingRecord.HostPrivateKey)); await sslStream.AuthenticateAsClientAsync( pairingRecord.DeviceCertificate.Subject, clientCertificates, SslProtocols.Tls12, checkCertificateRevocation : false).ConfigureAwait(false); this.stream = sslStream; }
public void CopyWithPrivateKeyForSsl_Works() { var pairingRecord = PairingRecord.Read(File.ReadAllBytes("Lockdown/0123456789abcdef0123456789abcdef01234567.plist")); using (var certificate = pairingRecord.HostCertificate.CopyWithPrivateKeyForSsl(pairingRecord.HostPrivateKey)) { Assert.NotNull(certificate.PrivateKey); // On Windows, because of a SCHANNEL bug (https://github.com/dotnet/runtime/issues/23749#issuecomment-485947319), private keys // for certificates cannot be marked as ephemeral because the in-memory TLS client certificate private key is not marshaled // between SCHANNEL and LSASS. Make sure the private key is not ephemeral. if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { var key = Assert.IsType <RSACng>(certificate.PrivateKey); Assert.False(key.Key.IsEphemeral); } } }
public async Task CreateAsync_Works_Async() { var pairingRecord = new PairingRecord(); var sessionResponse = new StartSessionResponse() { SessionID = "1234" }; var lockdownClientFactory = new Mock <LockdownClientFactory>(MockBehavior.Strict); var muxerClient = new Mock <MuxerClient>(MockBehavior.Strict); var context = new DeviceContext() { Device = new MuxerDevice(), PairingRecord = pairingRecord }; var lockdownClient = new Mock <LockdownClient>(MockBehavior.Strict); lockdownClientFactory .Setup(l => l.CreateAsync(default))
public void ToPropertyList_Works() { var raw = File.ReadAllText("Lockdown/0123456789abcdef0123456789abcdef01234567.plist"); var dict = (NSDictionary)XmlPropertyListParser.ParseString(raw); var pairingRecord = PairingRecord.Read(dict); var serializedDict = pairingRecord.ToPropertyList(); Assert.Equal(dict.Keys, serializedDict.Keys); foreach (var key in dict.Keys) { Assert.Equal(dict[key], serializedDict[key]); } var xml = serializedDict.ToXmlPropertyList(); Assert.Equal(raw, xml, ignoreLineEndingDifferences: true); }
public void Equals_Works(string systemBuid, string hostId, string otherSystemBuid, string otherHostId, bool equal, bool sameHashCode) { var record = new PairingRecord() { SystemBUID = systemBuid, HostId = hostId }; var other = new PairingRecord() { SystemBUID = otherSystemBuid, HostId = otherHostId }; Assert.Equal(equal, record.Equals(other)); Assert.Equal(equal, other.Equals(record)); Assert.Equal(equal, PairingRecord.Equals(other, record)); Assert.Equal(equal, PairingRecord.Equals(record, other)); Assert.Equal(equal, object.Equals(other, record)); Assert.Equal(equal, object.Equals(record, other)); Assert.Equal(sameHashCode, record.GetHashCode() == other.GetHashCode()); }
public void Read_Works() { var dict = (NSDictionary)PropertyListParser.Parse("Lockdown/0123456789abcdef0123456789abcdef01234567.plist"); var pairingRecord = PairingRecord.Read(dict); Assert.NotNull(pairingRecord.DeviceCertificate); Assert.Equal("879D15EC44D67A89BF0AC3C0311DA035FDD56D0E", pairingRecord.DeviceCertificate.Thumbprint); Assert.Equal("MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDA=", Convert.ToBase64String(pairingRecord.EscrowBag)); Assert.NotNull(pairingRecord.HostCertificate); Assert.Equal("EE63391AA1FBA937E2784CC7DAAA9C22BA223B54", pairingRecord.HostCertificate.Thumbprint); Assert.Equal("01234567-012345678901234567", pairingRecord.HostId); Assert.NotNull(pairingRecord.HostPrivateKey); Assert.Equal(2048, pairingRecord.HostPrivateKey.KeySize); Assert.NotNull(pairingRecord.RootCertificate); Assert.Equal("DB0F6BAA694FA99879281A388D170CCE1412AC92", pairingRecord.RootCertificate.Thumbprint); Assert.NotNull(pairingRecord.RootPrivateKey); Assert.Equal(2048, pairingRecord.RootPrivateKey.KeySize); Assert.Equal("01234567890123456789012345", pairingRecord.SystemBUID); Assert.Equal("01:23:45:67:89:ab", pairingRecord.WiFiMacAddress); }
public async Task PairAsync_Works_Async() { var pairingRecord = new PairingRecord(); var protocol = new Mock <LockdownProtocol>(MockBehavior.Strict); protocol .Setup(p => p.WriteMessageAsync(It.IsAny <LockdownMessage>(), default)) .Callback <LockdownMessage, CancellationToken>( (message, ct) => { var request = Assert.IsType <PairRequest>(message); Assert.Same(pairingRecord, request.PairRecord); Assert.NotNull(request.PairingOptions); Assert.True(request.PairingOptions.ExtendedPairingErrors); Assert.Equal("Pair", request.Request); }) .Returns(Task.CompletedTask); var dict = new NSDictionary(); protocol .Setup(p => p.ReadMessageAsync(default))
public void ToString_EmptyPairingRecord() { var record = new PairingRecord(); Assert.Equal("HostId: , SystemBUID: , Host certificate: (expires: )", record.ToString()); }
/// <summary> /// Asynchronously retrieves or generates a pairing record for a device. /// </summary> /// <param name="udid"> /// The UDID of the device for which to retrieve or generate the pairing record. /// </param> /// <param name="cancellationToken"> /// A <see cref="CancellationToken"/> which can be used to cancel the asynchronous operation. /// </param> /// <returns> /// A <see cref="Task"/> representing the asynchronous operation. /// </returns> public virtual async Task <PairingRecord> ProvisionPairingRecordAsync(string udid, CancellationToken cancellationToken) { // Pairing records can be stored at both the cluster level and locally. Use the first pairing record which is valid, and make sure the cluster // and local records are in sync. this.logger.LogInformation("Provisioning a pairing record for device {udid}", udid); var usbmuxdPairingRecord = await this.muxerClient.ReadPairingRecordAsync(udid, cancellationToken).ConfigureAwait(false); var kubernetesPairingRecord = await this.kubernetesPairingRecordStore.ReadAsync(udid, cancellationToken).ConfigureAwait(false); this.logger.LogInformation("Found pairing record {usbmuxdPairingRecord} in usbmuxd", usbmuxdPairingRecord); this.logger.LogInformation("Found pairing record {kubernetesPairingRecord} in Kubernetes", kubernetesPairingRecord); PairingRecord pairingRecord = null; using (var context = await this.serviceProvider.CreateDeviceScopeAsync(udid, cancellationToken).ConfigureAwait(false)) await using (var lockdown = await context.StartServiceAsync <LockdownClient>(cancellationToken).ConfigureAwait(false)) { if (usbmuxdPairingRecord != null && await lockdown.ValidatePairAsync(usbmuxdPairingRecord, cancellationToken).ConfigureAwait(false)) { this.logger.LogInformation("The pairing record stored in usbmuxd is valid."); pairingRecord = usbmuxdPairingRecord; } else if (kubernetesPairingRecord != null && await lockdown.ValidatePairAsync(kubernetesPairingRecord, cancellationToken).ConfigureAwait(false)) { this.logger.LogInformation("The pairing record stored in Kubernetes is valid."); pairingRecord = kubernetesPairingRecord; } else { this.logger.LogInformation("No valid pairing record could be found."); if (!this.pairingTasks.ContainsKey(udid) || this.pairingTasks[udid].IsCompleted) { this.logger.LogInformation("Starting a new pairing task"); var worker = context.ServiceProvider.GetRequiredService <PairingWorker>(); this.pairingTasks[udid] = worker.PairAsync(cancellationToken); } this.logger.LogInformation("A pairing task is running. Returning null and waiting for the device to pair with the host."); return(null); } } // Update outdated pairing records if required. if (!PairingRecord.Equals(pairingRecord, usbmuxdPairingRecord)) { this.logger.LogInformation("The pairing record stored in usbmuxd for device {device} is outdated. Updating.", udid); if (usbmuxdPairingRecord != null) { await this.muxerClient.DeletePairingRecordAsync(udid, cancellationToken).ConfigureAwait(false); } await this.muxerClient.SavePairingRecordAsync(udid, pairingRecord, cancellationToken).ConfigureAwait(false); this.logger.LogInformation("Updated the pairing record in usbmuxd for device {device}.", udid); } if (!PairingRecord.Equals(pairingRecord, kubernetesPairingRecord)) { this.logger.LogInformation("The pairing record stored in the cluster for device {device} is outdated. Updating.", udid); if (kubernetesPairingRecord != null) { await this.kubernetesPairingRecordStore.DeleteAsync(udid, cancellationToken).ConfigureAwait(false); } await this.kubernetesPairingRecordStore.WriteAsync(udid, pairingRecord, cancellationToken).ConfigureAwait(false); this.logger.LogInformation("Updated the pairing record in the cluster for device {device}.", udid); } return(pairingRecord); }
/// <inheritdoc/> public override Task WriteAsync(string udid, PairingRecord pairingRecord, CancellationToken cancellationToken) { return(this.muxer.SavePairingRecordAsync(udid, pairingRecord, cancellationToken)); }