Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CT-KIP support #27

Open
cemeyer opened this issue Apr 27, 2016 · 53 comments
Open

CT-KIP support #27

cemeyer opened this issue Apr 27, 2016 · 53 comments

Comments

@cemeyer
Copy link

cemeyer commented Apr 27, 2016

URLs look like com.rsa.securid://ctkip?url=https://XXX.com:443/ctkip/services/CtkipService and come with a 12 decimal digit activation code.

The protocol is documented in this RFC: https://tools.ietf.org/html/rfc4758

Edit 2019-01-27: Some exciting news! While not yet integrated into stoken, users with these activation tokens can derive the shared secret and import it into stoken with Dan Lenski's excellent https://github.com/dlenski/rsa_ct_kip client.py!

@pfps
Copy link

pfps commented Aug 17, 2016

Any plans for progress on this?

@dlenski
Copy link

dlenski commented Sep 6, 2018

I've got a MITMproxy capture of the whole process. It's not too complex but wrapped in a bunch of fugly XML two layers deep (with SOAP on top) and a "custom" AES-based MAC algorithm for everything. 🤦‍♂️.

For RSA SecurID client v5.0.2.440:

  1. the client says hello and sends the 12-digit activation code,
  2. the server sends back its RSA public key and a 16-byte nonce (MAC'ed),
  3. the client sends back a 16-byte nonce encrypted with that RSA public key via PKCS#1 OAEP
  4. The server replies with some details about the OTP output format, and confirms that it has grokked the whole conversation by sending a MAC of (a) the client and server nonces, which are straightforward, combined with (b) some K_AUTH mystery meat key that the RFC is very vague about.

By far, the most annoying part is going to be implementing the boutique CT-KIP-PRF-AES MAC algorithm (from Appendix D.2.2).

cemeyer added a commit to cemeyer/stoken that referenced this issue Sep 7, 2018
Add totally untested implementations of the OMAC1-AES ("CMAC") and
CT-KIP-PRF-* family of pseudo-random functions per relevant RFCs.  It
compiles but that's about all I've tested.

Broken:
* Only supports tomcrypt
* Untested!!!
* Mallocs in PRF-SHA256 variant to work around missing iterative
  sha256-hmac.  Could add iterative hmac instead and skip malloc.

Re: stoken-dev#27
@dlenski
Copy link

dlenski commented Sep 7, 2018

I've written a simple "fake" token activation server in Python+Flask. The official RSA SecurID client is willing to talk to it.

Gist with fake server: https://gist.github.com/dlenski/e4a53a17c0786f492fc04c901968681d

But I'm stuck on how to do the MAC required to validate this. There seem to be multiple layers of intentional obfuscation in terms of how to generate the shared keying material ☹️

@dlenski
Copy link

dlenski commented Sep 7, 2018

It may be more productive to try to "break" this from the client side, if you have access to a real server where you can generate a stream of new activation codes.

The client never has to MAC anything or decrypt anything.

The problem is that the 16-byte nonce sent by the client and the 16-byte nonce sent by the server almost certainly get combined in some obfuscated way that only the official RSA software really understands. So a naïve client will be able to deliver its 16-byte nonce to the server, which will then use it to build the final token seed, but figuring out what that final seed value is will prove difficult…

@cemeyer
Copy link
Author

cemeyer commented Sep 7, 2018

I don't have direct access to a server. I guess I could try asking my company's IT department for new activation codes repeatedly, but they might deny my requests at some point 😂.

Edit: Actually, if I just need to be able to access something like what I posted in the first comment, I may able to try things.

@dlenski
Copy link

dlenski commented Sep 11, 2018

Bringing the discussion back from my gist because you can't get notifications on Gists ☹️.

@cemeyer wrote:

I hope the RSA pubkey representation fed into the PRF isn't .der and especially PEM, but I don't really know. Could try the DER order (modulus n, followed by exponent e), but I don't know how long of an encoding e would have. Then again PKCS 1 specifies e=65537, so maybe just try the (big-endian, i.e., I2OSP) modulus (which should have an exact encoding width).

Yep, big-endian, modulus only seems plausible.

Yeah. I've hunted around a little bit and I'm having trouble finding any other CT-KIP-PRF implementation to compare against (not the same as known test vectors, but better than nothing). I wonder if the (single) MAC in https://tools.ietf.org/html/draft-nystrom-ct-kip-two-pass-01#page-30 is a valid test vector. But I have not investigated what it is supposed to be over, or if the key is even supplied.

Unfortunately not. Even with a complete, verbatim exchange, we have no idea what the MAC is over because the RSA private key is needed to decrypt the client nonce.

Yeah, we have a very low information oracle on whether we got it right. Hmm. Maybe reversing the client would be more straightforward.

Agreed. Upon my initial read of the RFC, I thought that reverse-engineering the server would be easier because the client software can be run privately while the server only hands out one-time activation codes to attempt (and I have to go through a grumpy IT department).

I think that writing a "working" client should actually be trivial, because the client doesn't actually have to sign anything or use any obfuscated algorithms. I put "working" in quotes because the likely outcome is that the server accepts the client's nonce, and then we have no clear idea what the resulting K_TOKEN is… but at least we then have a somewhat better oracle, in that we know both the client nonce and the server nonce, and can try generating the resulting codes with stoken and using them to log into [whatever the token is used for].

I guess I could try asking my company's IT department for new activation codes repeatedly…

Similar. It'd be awkward for me to ask for more than a "small-single digit" number of activation codes. 😂

@dlenski
Copy link

dlenski commented Sep 11, 2018

@cemeyer, here's a client implementation: dlenski/rsa_ct_kip:client.py.

Run as python3 client.py --verbose URL ACTIVATION_CODE.

… but at least we then have a somewhat better oracle, in that we know both the client nonce and the server nonce, and can try generating the resulting codes with stoken and using them to log into [whatever the token is used for].

Not really tested, but it should be enough to get this "better oracle", since it will print both server and client nonces (unencrypted), as well as the server "MAC"/PRF output.

@cemeyer
Copy link
Author

cemeyer commented Sep 11, 2018

Nice :-). So if someone is brave enough to use this, the client may actually correctly activate a key with the server, but we won't actually know what that key is :P. Although, with the client-side plaintexts+PRF output we can probably figure it out offline.

@cemeyer
Copy link
Author

cemeyer commented Jan 10, 2019

@dlenski Hey, so I got a new phone and have a legitimate need to request a new token anyway. What testing can I do with a fresh token that would be valuable? Is the Python client from two comments ago still a useful test? Thanks!

Edit: Digging through old email. Some alternate (Android?) form of the URL looks like:

http://127.0.0.1/securid/ctkip?scheme=https&url=foo.baz.com:443/ctkip/services/CtkipService

@dlenski
Copy link

dlenski commented Jan 11, 2019

Is the Python client from two comments ago still a useful test?

Yes, it should be. You'll get all the unencrypted nonces that way. If you can figure out how to combine them to get the actual seed for the token… then we've got ourselves a tool for destupidifying the RSA tokens. 👍

I haven't been able to do any further work on this since I no longer have access to a client that uses CT-KIP.

@cemeyer
Copy link
Author

cemeyer commented Jan 11, 2019

You've already got the server side recorded, right? Would it be a useful test to run a dry-run registration against a fake server with real data to capture the exchange (to whatever point it succeeds)? I've seen some documentation of the official RSA client being able to ignore untrusted CAs. Since the real server doesn't know the client has registered, I don't think that will use up the activation code.

Thanks!

@dlenski
Copy link

dlenski commented Jan 12, 2019

You've already got the server side recorded, right? Would it be a useful test to run a dry-run registration against a fake server with real data to capture the exchange (to whatever point it succeeds)?

Yes, but you can do this part at any time, using the ordinary client and fakeserver.py.

What do we learn from it? Unfortunately, not much. We confirm that the official client and fake server can indeed talk to each other, and we learn the value of the unencrypted client and server nonces… but the client doesn't accept the final "MAC" (which is probably not really a MAC… but whatever).

I've seen some documentation of the official RSA client being able to ignore untrusted CAs.

The official client doesn't pin its CAs in any way, as far as I can tell. If Windows trusts the CA, the official client will trust it too.

Since the real server doesn't know the client has registered, I don't think that will use up the activation code.

Correct. You can test against the fake server all you want, and the real server won't know anything about this.

Honestly, this whole CT-KIP provisioning thing is so unreliable that no IT department will be surprised if you have to request that they regenerate the activation code for you 5 times over a period of a day or two. :-P

Probably the right way to make progress here:

  1. Write a MITM version which acts as both a client and a server. It should pretend to be a real server to the client, and pretend to be a real client to the server.
  2. This MITM script can capture (a) the server nonce, which is always unencrypted anyway and (b) the unencrypted client nonce passed to the server and (c) the "MAC" returned by the real server to the client.
  3. But still, the likely outcome is that the "real" client rejects the MAC returned by the real server… because it's computed based on the real server's RSA key, rather than the MITM's RSA key. (If not, then this is a hilarious fail by RSA.)

@cemeyer
Copy link
Author

cemeyer commented Jan 12, 2019

Honestly, this whole CT-KIP provisioning thing is so unreliable that no IT department will be surprised if you have to request that they regenerate the activation code for you 5 times over a period of a day or two. :-P

💯

Re: suggested steps, I'll see what I can do 😂 😎

Edit: server uses 1024 bit RSA? Is that trivially factorable yet, or just 1-2 years away? 😂 😂 😂

@cemeyer
Copy link
Author

cemeyer commented Jan 14, 2019

FYI, https://community.rsa.com/docs/DOC-85572 confirms the device binding ID you preregister with the admin server is a 24-char hexadecimal string and is "generated" (suggests random, but could be computed or include a checkcode or something).

When a user attempts to import a token bound to a device, the RSA SecurID SDK gets
the binding ID from the token and checks whether the data matches the 24-character
hexadecimal string returned by getBindingId.

Not sure how that works with CT-KIP; I don't see it in the fakeserver.py. TokenID is probably the token serial number, which is different.

Re: key-is-computed-based-on-real-server's RSA, probably:

the CT-KIP process is engineered to prevent the potential interception of the token’s seed.

... use a four-pass CT-KIP protocol to exchange information that is used to dynamically generate a unique shared seed. Information critical to seed generation is encrypted during transmission using a public-private key pair. The generated token seed value is never transmitted across the network.

Well, we knew that.

Your mobile app would then call the importTokenFromCtkip:ctkipAuthCode:validateCert:delegate: method to pass in the underlying CT-KIP data.

If the SSL certificate that you use to secure your CT-KIP connections does not use SHA-256 or later, then you must replace it. The default RSA Authentication Manager SSL console certificates do not meet the ATS requirement.

(lol)

...

Import Procedure

call importTokenFromCtkip:ctkipAuthCode:validateCert:delegate: ... Pass in the URL ... to ctkipUrl, and pass in the activation code ... ctkipAuthCode.

@cemeyer
Copy link
Author

cemeyer commented Jan 14, 2019

I don't think I can get a MITM server set up in the timeframe I have available. I can certainly at least try the fake client and record some results. If the e.g. Windows SDK runs in wine that might be an interesting enough avenue for me, even if it doesn't work with stoken.

@dlenski
Copy link

dlenski commented Jan 14, 2019

The RSA docs are so convoluted and perhaps intentionally obfuscated that I don't get much out of that. The part about device binding is kind of interesting though: it seems to be saying that the "device binding" is enforced purely on the local side, by the RSA SecurID client software verifying that the host computer's device-binding-ID matches the one saved in the token. (In other words, a free implementation like stoken can just ignore it. 😂)

I can certainly at least try the fake client and record some results.

I say "go for it." Best case is that you capture all of the nonces and figure out how the token secret is generated. Worst case is smaller incremental progress.

Like I said, I no longer have access to a "real" RSA CT-KIP orac… er, generator… so I can't help. ☹️

@cemeyer
Copy link
Author

cemeyer commented Jan 14, 2019

Yeah, no worries. One other interesting tidbit is that the CT-KIP negotiation code in the official windows client does not appear to be obfuscated at all. There are direct strings of "ct-kip-prf-aes", "StartService", "application/vnd.otps.ct-kip", "SOAPAction", "EncryptedNonce" in rsatokenbroker.exe1 and sdui_softwaretoken.dll of the official 5.0.2 client that are pointed to directly from the code section. I suspect anyone familiar with basic windows RE techniques could extract the official client algorithm pretty easily (I'm not familiar, unfortunately).

@cemeyer
Copy link
Author

cemeyer commented Jan 14, 2019

Oh yeah and this may be helpful to someone working on a MITM server: cemeyer/rsa_ct_kip@21cef1d

@cemeyer
Copy link
Author

cemeyer commented Jan 15, 2019

Ok! Successful handshake between naive client and real server. Edit: KeyID and TokenID are identical and both match the "serial number" I was told ahead of time. Expiration is 2.75 years in the future, which seems a bit odd. So:

      K_TOKEN = CT-KIP-PRF (R_C, "Key generation" || k || R_S, <arbitray output length>)

      MAC = CT-KIP-PRF (K_AUTH, "MAC 2 computation" || R_C, len(R_C))

We know:

  • R_C (client nonce)
  • k (server RSA pubkey)
  • R_S (server nonce)
  • MAC

For the purposes of this document, the secret key k shall be 16 octets long

  • K_TOKEN len

If no authentication key is present in the token, and no K_TOKEN existed before the CT-KIP run, K_AUTH shall be the newly generated K_TOKEN.

  • K_AUTH = K_TOKEN

The piece we don't have here is exactly how the server's RSA pubkey is incorporated into the input for the PRF to generate K_TOKEN, but we should have everything we need.

@dlenski
Copy link

dlenski commented Jan 15, 2019

Edit: KeyID and TokenID are identical and both match the "serial number" I was told ahead of time.

I got the same result.

We know: <the four explicit parameters that are supposed to go into the construction of K_TOKEN>

… except for a sane definition of the #@*$&U! CT-KIP-PRF, including that important implicit parameter: "exactly how the server's RSA pubkey is incorporated into the input for the PRF."

… but we should have everything we need.

Agreed. You also now (hopefully) have a binary oracle that can tell you whether or not you've figured out the right K_TOKEN (hopefully = seed value as shown by stoken, without further obfuscatory layers).

@cemeyer
Copy link
Author

cemeyer commented Jan 15, 2019

… except for a sane definition of the #@*$&U! CT-KIP-PRF, including that important implicit parameter: "exactly how the server's RSA pubkey is incorporated into the input for the PRF."

You also now (hopefully) have a binary oracle that can tell you whether or not you've figured out the right K_TOKEN

Yeah, exactly!

@cemeyer
Copy link
Author

cemeyer commented Jan 17, 2019

I found the self-service portal 😂 😂 😂

Looks like they use the same RSA public key for all k. That might be useful.

@dlenski
Copy link

dlenski commented Jan 17, 2019

Nice! So you can generate new activation codes at will?

Looks like they use the same RSA public key for all k. That might be useful.

"for all k"… globally, anywhere? Or for all k handed out by the same server?

@cemeyer
Copy link
Author

cemeyer commented Jan 17, 2019

Nice! So you can generate new activation codes at will?

So far!

"for all k"… globally, anywhere? Or for all k handed out by the same server?

For all 2/2 tokens I've received for my particular LDAP user on the same server. Low sample size but if they were per-activation I'd expect to have seen different keys. But I see a different k than in your pasted example earlier, so probably not global.

@rgerganov
Copy link

Hey guys,

I have reverse engineered the mobile RSA SecurId app and found how the Mac from ServerFinished is used:

byte a[] = func1(client_nonce, server_nonce, rsa_modulus);
byte b[] = func2(client_nonce, a);
// "a" and "b" are byte arrays with 16 elements
for (int i = 0 ; i < 16 ; i++) {
  if (b[i] != mac[i]) {
    throw new InvalidMac();
  }
}

The byte array a is stored and byte array b is discarded, so I believe that a is the shared secret. The implementation of func1 and func2 is just byte twiddling, no 3rd party algorithms is used. Will upload their source tomorrow.

So now the question is how to derive the stoken seed from this 16 byte shared secret?

@cemeyer
Copy link
Author

cemeyer commented Jan 21, 2019

func1 is some function of "Key generation", R_C (client_nonce), R_S (server nonce), and k (apparently just the RSA modulus, nice!). func2 is probably some function of "MAC 2 computation" and R_C. I'm curious about what particular function of R_C, R_S, and k func1 is — I tried modulus-only, but without any padding, and it didn't seem to match.

I think we're hoping the stoken seed is just the shared secret.

@rgerganov
Copy link

I have uploaded the sources: https://github.com/rgerganov/ctkip

There is also a Main class which invokes the decompiled RSA source with pre-recorded R_C, R_S and RSA modulus. However I get ArrayIndexOutOfBoundsException. I managed to fix some trivial compile errors in the decompiled source (see git history) but this is something different. Still looking.

@rgerganov
Copy link

OK, I upgraded jadx to its latest version and now the decompiled version of com.rsa.ctkip.b.a.a works. However, the produced MAC doesn't match the recorded value from cemeyer/rsa_ct_kip:client.py. Btw, I had to make some slight changes to this client to make it work and posted a PR. Am I doing something wrong on the client part?

I believe we are pretty close ...

@rgerganov
Copy link

I fixed the magic string in com.rsa.ctkip.b.a.c and now it works 🎉

Any idea how to import the 16 byte secret into stoken?

@dlenski
Copy link

dlenski commented Jan 22, 2019

I fixed the magic string in com.rsa.ctkip.b.a.c and now it works tada

Wow, this is impressive work… hooray!

Any idea how to import the 16 byte secret into stoken?

This is a good question. I believe you would have to basically use securid_encode_token to convert it into a form that stoken will accept… seems kind of silly to jump through this hoop but I don't know a better way to do it :(

@cernekee
Copy link
Collaborator

If you have a raw 16-byte dec_seed, you can base64-encode it and write it into an XML template file, then ask stoken to construct a new token file from it:

$ cat tpl.xml
<?xml version="1.0"?>
<TKNBatch>
  <TKN>
    <SN>000000000001</SN>
    <Death>2020/01/01</Death>
    <Seed>=AAECAwQFBgcICQoLDA0ODw==</Seed>
  </TKN>
</TKNBatch>
$ echo "AAECAwQFBgcICQoLDA0ODw==" | base64 -d | hexdump -C 
00000000  00 01 02 03 04 05 06 07  08 09 0a 0b 0c 0d 0e 0f  |................|
00000010
$ stoken export --random --sdtid --template tpl.xml > token.xml
$ stoken show --file=token.xml --seed
Serial number           : 000000000001
Decrypted seed          : 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 
Encrypted seed          : b7 ff a6 f9 9f 57 f0 16 90 15 b6 25 4a b6 24 5d 
Encrypted w/password    : no
Encrypted w/devid       : no
Expiration date         : 2020/01/02
Key length              : 128
Tokencode digits        : 8
PIN mode                : 3
Seconds per tokencode   : 60
App-derived             : no
Feature bit 4           : no
Time-derived            : yes
Feature bit 6           : no

The extra '=' at the start of the base64 string is mandatory (the RSA tools require it).

@cemeyer
Copy link
Author

cemeyer commented Jan 22, 2019

However, the produced MAC doesn't match the recorded value from cemeyer/rsa_ct_kip:client.py. Btw, I had to make some slight changes to this client to make it work and posted a PR. Am I doing something wrong on the client part?

Probably I was doing it wrong. :-)

The trick seems to be K_TOKEN = ct-kip-prf-aes(R_C, k || "Key generation" || R_S) rather than "Key generation" || k || R_S as the RFC claims 😱 . Also k = base64( long_to_bytes(pubk.n) ). I'm still missing something because I'm not getting the expected MAC, though. I've cleaned up the a.java class somewhat for clarity and can post it somewhere.

Edit: oh ffs, the string is Computation, not computation. RFC strikes again.

Edit2: Oh I see, you are decoding the base64 modulus, not leaving it as-is. I'm still not getting the right MAC in Python but I'm sure I'm doing something wrong. Hm.

Edit3: Oh, ok, ct-kip-prf is also documented out of order w.r.t. implementation. The block number is concatenated after the input msg, not before it. Now I get the correct MAC in Python. Stupid RFC.

Edit4: https://gist.github.com/cemeyer/7ebafafc616830faf6fec5c9f1abaa9b is the slightly less cryptic i.java class. I swapped the order of the b() methods but otherwise left order alone. I changed some method names inside that file to make it clearer what they did.

@cemeyer
Copy link
Author

cemeyer commented Jan 22, 2019

Working Python implementation: https://gist.github.com/cemeyer/3293e4fcb3013c4ee2d1b6005e0561bf

@dlenski
Copy link

dlenski commented Jan 22, 2019

This is awesome 😍

Do I have this right?

  1. First, I wrote a crude client dlenski:rsa_ct_kip/client.py that would mimic the RSA client and talk to the provisioning server (cleaned-up version: cemeyer:rsa_ct_kip/client.py).
  2. @rgerganov and @cemeyer have a working ct_kip_prf_aes in Python which will allow the client to derive the token secret and the (MAC matching the ServerFinished message).
  3. @cernekee explained how to load the the token into stoken from the (seed, expiration date, token ID) without wholly re-implementing the token encoding algorithm.

So can it all be put together… can you make the the Python client verify the MAC and create a working token?

@dlenski
Copy link

dlenski commented Jan 22, 2019

Oh, ok, ct-kip-prf is also documented out of order w.r.t. implementation. The block number is concatenated after the input msg, not before it. Now I get the correct MAC in Python. Stupid RFC.

In other words, there would be no chance of implementing RSA's convoluted "open standard" in a way that interoperates with their software without reverse engineering… because they got the order wrong in Section 3.5 of RFC4758 🤦‍♂️

K_TOKEN computation described in Section 3.5:

--- K_TOKEN = CT-KIP-PRF (R_C, "Key generation" || k || R_S, dsLen)

What RSA SecurID software apparently actually does.. to compute K_TOKEN:

+++ K_TOKEN = CT-KIP-PRF (R_C, k || "Key generation" || R_S, dsLen)

@cemeyer
Copy link
Author

cemeyer commented Jan 22, 2019

Yeah. They also got §3.8.6 wrong:

--- MAC = CT-KIP-PRF (K_AUTH, "MAC 2 computation" || R_C, dsLen)
+++ MAC = CT-KIP-PRF (K_AUTH, "MAC 2 Computation" || R_C, dsLen)

And §D.2.2:

--- F (k, s, i) = OMAC1-AES (k, INT (i) || s)
+++ F (k, s, i) = OMAC1-AES (k, s || INT (i))

It's hard to see this as unintentional, IMO.

Edit: stoken export doesn't really work unless you already have a token, I think. Looking at the code I don't see how it can work without one.

@dlenski
Copy link

dlenski commented Jan 22, 2019

It's hard to see this as unintentional, IMO.

I'm not sure if it's malice or incompetence… but not surprised that it's full of mistakes 😡. My first take when we were discussing the RFC is that it was incredibly sloppily written, and that it appeared to be describing the operation of an existing program, rather than actually a genuine attempt to describe a new standard.

stoken export doesn't really work unless you already have a token, I think. Looking at the code I don't see how it can work without one.

  1. It does work without .stokenrc file if you use the --random flag. At least v0.9 does.
  2. You have to do an export to the long-winded XML format (--sdtid) in order for the parameters in the --template to be incorporated. If you export to the default CTF ("compressed token format"), the values in the --template will be ignored… it'll just give you a random token.
  3. It would be nice to have a way to import into stoken using "just" the decrypted seed and other values shown in stoken show, but that would basically mean reimplementing the fiddly bits of v2_encode_token which I mentioned above.
$ stoken show --seed=XXXXXXXXXXXXX
Serial number           : XXXXXXXXXXX
Decrypted seed          : 16 bytes in hex
Encrypted seed          : 16 bytes in hex
Encrypted w/password    : no
Encrypted w/devid       : no
Expiration date         : YYYY/MM/DD
Key length              : 128
Tokencode digits        : 8
PIN mode                : 2
Seconds per tokencode   : 60
App-derived             : no
Feature bit 4           : no
Time-derived            : yes
Feature bit 6           : no

@dlenski
Copy link

dlenski commented Jan 22, 2019

I made a gist with make_RSA_token.sh to illustrate this.

Provide it with the seed (32 hex digits), serial number, and expiration date, and it'll convert it to CTF format:

# Run like this
$ SEED=000102030405060708090a0b0c0d0e0f SN=12345678901 EXPIRATION=2030/12/31 \
>    ./make_RSA_token.sh 
Serial Number: 12345678901
Expiration (YYYY/MM/DD): 2030/12/31
Seed (hex): 000102030405060708090a0b0c0d0e0f
Seed (base64): AAECAwQFBgcICQoLDA0ODw==
Compressed token format: 201234567890173042071776752216050450744647071253341607664173154164001302716774734

You can use stoken show that this token has the intended seed value:

$ stoken show --seed --token=201234567890173042071776752216050450744647071253341607664173154164001302716774734
...
Decrypted seed          : 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f 
...

cemeyer added a commit to cemeyer/stoken that referenced this issue Jan 22, 2019
Add totally untested implementations of the OMAC1-AES ("CMAC") and
CT-KIP-PRF-AES pseudo-random function per relevant RFCs (but see also:
mistakes in the RFC mentioned in stoken-dev#27).  It compiles but that's about all
I've tested.

Broken:
* Only supports tomcrypt
* Untested!!!

Re: stoken-dev#27
@cemeyer
Copy link
Author

cemeyer commented Jan 22, 2019

I've fixed the C implementation of ct_kip_prf_aes in my stoken fork on GH to match the implementation as described earlier on this thread and just went ahead and removed the -SHA variant. I still haven't tested it at all yet, and obviously it's just a small piece of this. I have to run for now but might be able to play with this later. Thanks everybody.

@dlenski
Copy link

dlenski commented Jan 23, 2019

The latest version of dlenski/rsa_ct_kip incorporates the ct_kip_prf_aes from @rgerganov and @cemeyer.

Arguments for client.py:

$ ./client.py --help
usage: client.py [-h] [-v] [-k] url activation_code

positional arguments:
  url
  activation_code

optional arguments:
  -h, --help       show this help message and exit
  -v, --verbose
  -k, --no-verify  Don't verify server TLS cert

This client.py can talk to fakeserver.py, do the full exchange, get the K_TOKEN and verify the MAC:

$ ./client.py -k https://localhost:4443 foobar

Got server nonce and RSA pubkey:
b'ad10736d27ed143064b2a506c6d67a7e'
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDiiEzL+4HtZrSfbLXYs71Fe0r/
bEz4mMOdpubxyNgGjjx7NJj02dXkHqkPVnUHLR6h3ZuU6OfmU/PpBEWK9lUVrs6B
AA2psVA7BvrZ+JbZC0YcIJ7kt3bsAkYiv37zxhfVKQZr3EoH55ewWfTZ8p5y09y2
qDwc7WzgxGQBaRiecQIDAQAB
-----END PUBLIC KEY-----

Generated client nonce:
	plaintext: b'd64949a1cb1b0bf6ed4d741be965bcc6'
	encrypted: b'dcf16b7ff866c760b6d3387d8d45c02b2adb4373eb9f7ecc7453e26d280452fd0af3b7e5b271c531f3bbba37d6476fbc6c1ef67240937d60cc08e52402e6a4cbf064b4dabf644929ef645929895a4d845e8912b4cf68dcfab8a3f3616ce0074c68fdc46d0548ff8b03bb7277b40fddb917420ab2b03c545f8cec0c6ebcc51bf9'

Got key ID, token ID, key expiration date, and MAC:
KeyID: b'247463689455'
TokenID: b'247463689455'
Expiration: 2020-01-22T00:00:00+00:00
MAC verified. K_TOKEN is: b'f42c449973d94de24fd204b3aaecc51a'

And correspondingly from the server log (filtering out the junk):

ENcrypted ClientNonce: b'dcf16b7ff866c760b6d3387d8d45c02b2adb4373eb9f7ecc7453e26d280452fd0af3b7e5b271c531f3bbba37d6476fbc6c1ef67240937d60cc08e52402e6a4cbf064b4dabf644929ef645929895a4d845e8912b4cf68dcfab8a3f3616ce0074c68fdc46d0548ff8b03bb7277b40fddb917420ab2b03c545f8cec0c6ebcc51bf9'
DEcrypted ClientNonce: b'd64949a1cb1b0bf6ed4d741be965bcc6'
K_TOKEN (hex):  b'f42c449973d94de24fd204b3aaecc51a'
MAC (hex):  b'b57de579c34292aac7e905ede73fa286'

However, I still can't get the real RSA client to accept the MAC sent by fakeserver.py, even after trying to slavishly replicate the exact whitespace idiosyncracies of the real server. ☹️

If any other brave soul would like to try running client.py against a real server, and using make_RSA_token.sh to convert the raw K_TOKEN into something stoken can use… and then finding out if it actually generates the correct codes… that'd be swell. Only do this if you can easily request new activation codes 😀

@rgerganov
Copy link

@cernekee Thank you so much, it worked! The only catch was that I had to set the serial number to match the TokenId from the ServerFinished message. Now I can login with tokens generated by stoken 😄 🎉

@dlenski @cemeyer I will start looking into the python stuff now

@rgerganov
Copy link

@dlenski Hm, the fakeserver.py doesn't work for me either (I am using the RSA SecurId for Android) although it generates the correct MAC. By the way you can run the whole thing over plain HTTP by using the following URL format: http://127.0.0.1/securid/ctkip?scheme=http&url=<fake_server_host>:<fake_server_port>

I can see the whole exchange of messages but at the end the mobile app says Token import failed.

@dlenski
Copy link

dlenski commented Jan 23, 2019

Hm, the fakeserver.py doesn't work for me either (I am using the RSA SecurId for Android) although it generates the correct MAC. … I can see the whole exchange of messages but at the end the mobile app says Token import failed.

@rgerganov, this is the same problem that I'm having. How can we be sure that it's the correct MAC, since nothing else seems to be wrong?

If you use the client.py with a real server, does it actually work to get you the correct K_TOKEN?

@rgerganov
Copy link

If you use the client.py with a real server, does it actually work to get you the correct K_TOKEN?

Yes. I get the shared secret (K_TOKEN) and TokenID with client.py, then create token.xml, import into stoken and successfully generate valid tokens against a real server.

@cemeyer
Copy link
Author

cemeyer commented Jan 23, 2019

Excellent! Sounds like we have all of the pieces!

@dlenski
Copy link

dlenski commented Jan 23, 2019

Eureka!

It's all working now over at https://github.com/dlenski/rsa_ct_kip.

The real RSA windows client will now talk to the fake server (the real client is stupidly fiddly about the exact formatting of the XML messages… I don't think it really parses them) and I confirm that it produces the expected token codes.

The Python client can talk to the real RSA server, provision a token, and explain how to use the output with stoken:

$ ./client.py https://server.company.com:443/ctkip/services/CtkipService ACTIVATION_CODE -s template.xml
Sending ClientHello request to server...
Received ServerHello response with server nonce (R_S = 28198dbe2c18a00335179cc5bb4eff3a) and 1024-bit RSA public key
Generated client nonce (R_C = 12bec1a6f4d09470986b485561c4d2b5)
Sending ServerFinished request to server, with encrypted client nonce...
MAC verified (0f103bc63a8819ffdbee657d042144f6)
Received ServerFinished response with token information:
  Key ID: 838999658504
  Token ID: 838999658504
  Token User:
  Expiration date: 2020-01-23T00:00:00+00:00
  OTP mode: 8 Decimal, every 60 seconds
  Token seed: 30ade1be20b3867d967bd2927c8eb0ca
Saved template to template.xml. Convert to XML format (.sdtid) with:
  stoken export --random --sdtid --template=template.xml > 838999658504.sdtid

@jcpunk
Copy link

jcpunk commented Jan 23, 2019

That is AWESOME!

Any chance to get the client bits ported into stoken so that it can fetch from ctkip?

@dlenski
Copy link

dlenski commented Jan 23, 2019

Any chance to get the client bits ported into stoken so that it can fetch from ctkip?

This is a job for someone who has an appetite to (re)write a bunch of extraordinarily ugly XML-and-web-services code in C. 😂

For now… there's a functional solution in Python for anyone who just needs to provision a working token.

@rgerganov
Copy link

Eureka!

It's all working now over at https://github.com/dlenski/rsa_ct_kip.

YES! I confirm it's also working with the mobile version. Great work :)

@cemeyer
Copy link
Author

cemeyer commented Jan 24, 2019

I was able to resync my token successfully but I'm not sure what my pin is and locked myself out 😂 . Not stoken's fault, seems to be working (or resync would have failed).

Edit: can confirm, stoken works with the CT-KIP shared secret as seed after PIN reset.

@dlenski
Copy link

dlenski commented Jan 28, 2019

@cemeyer, might be a good idea for you to update your original post with a link to the Python tool if you think it's a worthy solution. It can now be auto-installed with pip3, and I hope it'll Just Work™ for most people.

@cemeyer
Copy link
Author

cemeyer commented Jan 28, 2019

Yep, good idea. I'll do so. I'd like to see native support, and this ticket can track that, but for now we have a usable workaround.

Edit: RFC errata submitted.

@dlenski
Copy link

dlenski commented Feb 13, 2019

Edit: RFC errata submitted.

Good on you for submitting this. 👌

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants