ADS.Secure (yaads v0.1.0)

Copy Markdown View Source

Secure ADS: ADS inside TLS 1.2, to port 8016, from TwinCAT 3.1 build 4024 on.

Beckhoff's manual, "TwinCAT 3 Secure ADS", describes the three ways the two ends agree on keys, and how to set them up on the PLC; what goes inside the TLS connection it leaves out. That part is as described by https://github.com/kevinherron/beckhoff-secure-ads, and is yet to be checked against a PLC:

  1. TLS 1.2, with a certificate on both sides, or a pre-shared key.
  2. The client sends a TlsConnectInfo, and the PLC answers with one (connect_info/4).
  3. AMS packets follow, each without the AMS/TCP header in front, as the AMS header holds their length (see ADS.AMS).
ModeKeysOn the PLC
Pre-shared keyan identity and a password, and TLS-PSK with the key psk_key/2an entry under Server/Tls/Psk in its StaticRoutes.xml
Shared CAa certificate for each side from a CA both trustthe CA, its own certificate and key under Server/Tls
Self-signedeach side its own certificate, the PLC's known by its SHA-256 fingerprinta route, added once with the login of a user on the PLC (add_route:)

A TlsConnectInfo is 64 bytes, with a user's name and password after them when adding a route:

BytesField
2length, 64 and the credentials
2flags
1version, 1
1error, in the PLC's answer: 0, or 1 for the TLS version, 2 a certificate's name, 3 an unknown certificate, 4 an unknown user
6the NetId of the sender
1, 1lengths of the user name and the password
18reserved, zeros
32the sender's host name, with zeros after
...the user name and the password

Its strings are in Windows-1252, as are the PSK identity and password.

The flags are, from the lowest bit: :response, in the PLC's answer; :ams_allowed; :server_info; :own_file; :self_signed, for self-signed certificates; :ip_address, for a route that names the client by its address rather than its host name; :ignore_cn, for certificates whose common name isn't the name of their host; and :add_remote, to add a route.

ADS.Client takes all this as secure:; this module has the parts, for tests and those who bring their own transport.

Summary

Functions

A TlsConnectInfo, with credentials to add a route, or nil; the strings in the code page (see ADS.Codepage). The host name is cut to 31 bytes.

The first TlsConnectInfo in the bytes: {:ok, info, rest}, :more for the start of one, or :error for one that can't be, longer than 512 bytes or shorter than 64. The strings are in the code page.

A certificate's fingerprint as Secure ADS compares them: the SHA-256 of its DER, as 64 hexadecimal digits; the same as ADS.Certificate.fingerprint/1.

The pre-shared key of an identity and a password, as TwinCAT makes it: the SHA-256 of the identity in upper case followed by the password, both in the code page.

Types

connect_info()

@type connect_info() :: %{
  flags: [flag()],
  version: byte(),
  error:
    nil
    | :version
    | :cn_mismatch
    | :unknown_certificate
    | :unknown_user
    | byte(),
  net_id: ADS.NetId.t(),
  hostname: String.t(),
  credentials: {String.t(), String.t()} | nil
}

flag()

@type flag() ::
  :response
  | :ams_allowed
  | :server_info
  | :own_file
  | :self_signed
  | :ip_address
  | :ignore_cn
  | :add_remote

Functions

connect_info(flags, net_id, hostname, credentials \\ nil, codepage \\ Codepage.default())

@spec connect_info(
  [flag()],
  ADS.NetId.t(),
  String.t(),
  {String.t(), String.t()} | nil,
  ADS.Codepage.t()
) :: binary()

A TlsConnectInfo, with credentials to add a route, or nil; the strings in the code page (see ADS.Codepage). The host name is cut to 31 bytes.

iex> info = ADS.Secure.connect_info([:self_signed], <<10, 0, 0, 7, 1, 1>>, "scada")
iex> byte_size(info)
64
iex> {:ok, %{flags: [:self_signed], hostname: "scada"}, ""} = ADS.Secure.decode_connect_info(info)

decode_connect_info(bytes, codepage \\ Codepage.default())

@spec decode_connect_info(binary(), ADS.Codepage.t()) ::
  {:ok, connect_info(), binary()} | :more | :error

The first TlsConnectInfo in the bytes: {:ok, info, rest}, :more for the start of one, or :error for one that can't be, longer than 512 bytes or shorter than 64. The strings are in the code page.

fingerprint(der)

@spec fingerprint(binary()) :: String.t()

A certificate's fingerprint as Secure ADS compares them: the SHA-256 of its DER, as 64 hexadecimal digits; the same as ADS.Certificate.fingerprint/1.

psk_key(identity, password, codepage \\ Codepage.default())

@spec psk_key(String.t(), String.t(), ADS.Codepage.t()) :: <<_::256>>

The pre-shared key of an identity and a password, as TwinCAT makes it: the SHA-256 of the identity in upper case followed by the password, both in the code page.