예제 #1
0
        /// <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);
                }
            }
        }
예제 #2
0
        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));
        }
예제 #3
0
        /// <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);
            }
        }
예제 #4
0
        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());
        }
예제 #5
0
        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"));
        }
예제 #6
0
        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);
        }
예제 #7
0
        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();
        }
예제 #8
0
        /// <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;
        }
예제 #9
0
        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);
                }
            }
        }
예제 #10
0
        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))
예제 #11
0
        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);
        }
예제 #12
0
        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());
        }
예제 #13
0
        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);
        }
예제 #14
0
        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))
예제 #15
0
        public void ToString_EmptyPairingRecord()
        {
            var record = new PairingRecord();

            Assert.Equal("HostId: , SystemBUID: , Host certificate:  (expires: )", record.ToString());
        }
예제 #16
0
        /// <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);
        }
예제 #17
0
 /// <inheritdoc/>
 public override Task WriteAsync(string udid, PairingRecord pairingRecord, CancellationToken cancellationToken)
 {
     return(this.muxer.SavePairingRecordAsync(udid, pairingRecord, cancellationToken));
 }