How to send a challenge request via PEAP-GTC

classic Classic list List threaded Threaded
6 messages Options
| Threaded
Open this post in threaded view
|

How to send a challenge request via PEAP-GTC

ngoetz24
Is it possible to send a challenge response to a user asking them to enter a
OPT (One Time Password) token using PEAP with GTC?  I have followed the
documentation example and got this working with PAP, but our security team
will not allow us to use PAP due to security concerns with the week
encryption used by PAP.  

 

I also tried to do this via EAP-TTLS, but when I posted the issue I was
having, I was informed that EAP-TTLS has a fixed packet flow, and that I
should use EAP-GTS in the inner tunnel.  I am trying to do this now, but I
am obviously doing something wrong.  I am trying to follow the
documentation, but I could not find any examples of sending a challenge via
PEAP with GTC.  Is this even possible, or is PAP the only way to send a
challenge request to the user?

 

The problem I seem to be having is that when I use "challenge" in the
authenticate section of the inner-tunnel configuration it seems to break the
tunnel.  When I do this I get the following error message in the debug:

eap: ERROR: Failed continuing EAP GTC (6) session.  EAP sub-module failed.

 

My guess is that I am trying to do the challenge from the wrong location in
the configuration, but I am not sure where the proper place is.

 

My inner-tunnel is configured as follows:

 

 

server inner-tunnel {

listen {

       ipaddr = 127.0.0.1

       port = 18120

       type = auth

}

 

authorize {

                filter_username

                chap

                mschap

                suffix

                update control {

                                &Proxy-To-Realm := LOCAL

                }

                eap {

                                ok = return

                }

                files

                pap

}

 

authenticate {

                Auth-Type PAP {

                                pap

                }

 

                Auth-Type MSCHAP {

                                mschap

                }

 

                mschap

 

                Auth-Type ntlm_auth {

                                ntlm_auth

                                                if (ok) {

                                                                update reply
{

 
# Create a random State attribute:

 
State := "%{randstr:aaaaaaaaaaaaaaaa}"

 
Reply-Message := "Please enter OTP"

                                                                }

                                                                # Return
Access-Challenge:

                                                                challenge

                                                }


                }

                eap

}

 

session {

                radutmp

}

 

post-auth {

                if (0) {

                                update reply {

                                                User-Name !* ANY

                                                Message-Authenticator !* ANY

                                                EAP-Message !* ANY

                                                Proxy-State !* ANY

                                                MS-MPPE-Encryption-Types !*
ANY

                                                MS-MPPE-Encryption-Policy !*
ANY

                                                MS-MPPE-Send-Key !* ANY

                                                MS-MPPE-Recv-Key !* ANY

                                }

 

                                update {

                                                &outer.session-state: +=
&reply:

                                }

                }

 

                Post-Auth-Type REJECT {

                                -sql

                                attr_filter.access_reject

                                update outer.session-state {

                                                &Module-Failure-Message :=
&request:Module-Failure-Message

                                }

                }

}

 

pre-proxy {

}

 

post-proxy {

                eap

}

 

}

 

Here is a copy of the section of the debug where it seems to break:

 

(18) eap_peap: Continuing EAP-TLS

(18) eap_peap: Peer indicated complete TLS record size will be 42 bytes

(18) eap_peap: Got complete TLS record (42 bytes)

(18) eap_peap: [eaptls verify] = length included

(18) eap_peap: [eaptls process] = ok

(18) eap_peap: Session established.  Decoding tunneled attributes

(18) eap_peap: PEAP state phase2

(18) eap_peap: EAP method GTC (6)

(18) eap_peap: Got tunneled request

(18) eap_peap:   EAP-Message = 0x020800110643306b3343306b3331323334

(18) eap_peap: Setting User-Name to test-user

(18) eap_peap: Sending tunneled request to inner-tunnel

(18) eap_peap:   EAP-Message = 0x020800110643306b3343306b3331323334

(18) eap_peap:   FreeRADIUS-Proxied-To = 127.0.0.1

(18) eap_peap:   User-Name = " test-user "

(18) eap_peap:   State = 0x48f270f749fa76fc54bf6bce0580ffa4

(18) eap_peap:   Framed-MTU = 1200

(18) eap_peap:   Service-Type = Framed-User

(18) eap_peap:   NAS-Identifier = "Test"

(18) eap_peap:   NAS-IP-Address = 151.143.16.5

(18) eap_peap:   Event-Timestamp = "Sep 11 2019 10:09:42 PDT"

(18) Virtual server inner-tunnel received request

(18)   EAP-Message = 0x020800110643306b3343306b3331323334

(18)   FreeRADIUS-Proxied-To = 127.0.0.1

(18)   User-Name = " test-user "

(18)   State = 0x48f270f749fa76fc54bf6bce0580ffa4

(18)   Framed-MTU = 1200

(18)   Service-Type = Framed-User

(18)   NAS-Identifier = "Test"

(18)   NAS-IP-Address = 151.143.16.5

(18)   Event-Timestamp = "Sep 11 2019 10:09:42 PDT"

(18) server inner-tunnel {

(18)   session-state: No cached attributes

(18)   # Executing section authorize from file
/etc/raddb/sites-enabled/inner-tunnel

(18)     authorize {

(18)       policy filter_username {

(18)         if (&User-Name) {

(18)         if (&User-Name)  -> TRUE

(18)         if (&User-Name)  {

(18)           if (&User-Name =~ / /) {

(18)           if (&User-Name =~ / /)  -> FALSE

(18)           if (&User-Name =~ /@[^@]*@/ ) {

(18)           if (&User-Name =~ /@[^@]*@/ )  -> FALSE

(18)           if (&User-Name =~ /\.\./ ) {

(18)           if (&User-Name =~ /\.\./ )  -> FALSE

(18)           if ((&User-Name =~ /@/) && (&User-Name !~ /@(.+)\.(.+)$/))  {

(18)           if ((&User-Name =~ /@/) && (&User-Name !~ /@(.+)\.(.+)$/))
-> FALSE

(18)           if (&User-Name =~ /\.$/)  {

(18)           if (&User-Name =~ /\.$/)   -> FALSE

(18)           if (&User-Name =~ /@\./)  {

(18)           if (&User-Name =~ /@\./)   -> FALSE

(18)         } # if (&User-Name)  = notfound

(18)       } # policy filter_username = notfound

(18)       [chap] = noop

(18)       [mschap] = noop

(18) suffix: Checking for suffix after "@"

(18) suffix: No '@' in User-Name = " test-user ", looking up realm NULL

(18) suffix: No such realm "NULL"

(18)       [suffix] = noop

(18)       update control {

(18)         &Proxy-To-Realm := LOCAL

(18)       } # update control = noop

(18) eap: Peer sent EAP Response (code 2) ID 8 length 17

(18) eap: No EAP Start, assuming it's an on-going EAP conversation

(18)       [eap] = updated

(18)       [files] = noop

(18)       [pap] = noop

(18)     } # authorize = updated

(18)   Found Auth-Type = eap

(18)   # Executing group from file /etc/raddb/sites-enabled/inner-tunnel

(18)     authenticate {

(18) eap: Expiring EAP session with state 0x48f270f749fa76fc

(18) eap: Finished EAP session with state 0x48f270f749fa76fc

(18) eap: Previous EAP request found for state 0x48f270f749fa76fc, released
from the list

(18) eap: Peer sent packet with method EAP GTC (6)

(18) eap: Calling submodule eap_gtc to process data

(18) eap_gtc: # Executing group from file
/etc/raddb/sites-enabled/inner-tunnel

(18) eap_gtc:   Auth-Type ntlm_auth {

(18) ntlm_auth: Executing: /usr/bin/ntlm_auth --request-nt-key
--domain=EDD_LOCAL --username=%{mschap:User-Name}
--password=%{User-Password}:

(18) ntlm_auth: EXPAND --username=%{mschap:User-Name}

(18) ntlm_auth:    --> --username= test-user

(18) ntlm_auth: EXPAND --password=%{User-Password}

(18) ntlm_auth:    --> --password=Test-Password

(18) ntlm_auth: Program returned code (0) and output 'NT_STATUS_OK: The
operation completed successfully. (0x0)'

(18) ntlm_auth: Program executed successfully

(18)     [ntlm_auth] = ok

(18)     if (ok) {

(18)     if (ok)  -> TRUE

(18)     if (ok)  {

(18)       update reply {

(18)         EXPAND %{randstr:aaaaaaaaaaaaaaaa}

(18)            --> 96SQAtQhOkKcI9Gb

(18)         State := 0x39365351417451684f6b4b6349394762

(18)         Reply-Message := "Please enter OTP"

(18)       } # update reply = noop

(18)       policy challenge {

(18)         update control {

(18)           &Response-Packet-Type = Access-Challenge

(18)         } # update control = noop

(18)         [handled] = handled

(18)       } # policy challenge = handled

(18)     } # if (ok)  = handled

(18)   } # Auth-Type ntlm_auth = handled

(18) eap: ERROR: Failed continuing EAP GTC (6) session.  EAP sub-module
failed

(18) eap: Sending EAP Failure (code 4) ID 8 length 4

(18) eap: Failed in EAP select

(18)       [eap] = invalid

(18)     } # authenticate = invalid

(18)   Failed to authenticate the user

(18)   Using Post-Auth-Type Reject

(18)   # Executing group from file /etc/raddb/sites-enabled/inner-tunnel

(18)     Post-Auth-Type REJECT {

(18) attr_filter.access_reject: EXPAND %{User-Name}

(18) attr_filter.access_reject:    --> test-user

(18) attr_filter.access_reject: Matched entry DEFAULT at line 11

(18)       [attr_filter.access_reject] = updated

(18)       update outer.session-state {

(18)         &Module-Failure-Message := &request:Module-Failure-Message ->
'eap: Failed continuing EAP GTC (6) session.  EAP sub-module failed'

(18)       } # update outer.session-state = noop

(18)     } # Post-Auth-Type REJECT = updated

(18) } # server inner-tunnel

(18) Virtual server sending reply

(18)   Reply-Message := "Please enter OTP"

(18)   EAP-Message = 0x04080004

(18)   Message-Authenticator = 0x00000000000000000000000000000000

(18) eap_peap: Got tunneled reply code 3

(18) eap_peap:   Reply-Message := "Please enter OTP"

(18) eap_peap:   EAP-Message = 0x04080004

(18) eap_peap:   Message-Authenticator = 0x00000000000000000000000000000000

(18) eap_peap: Got tunneled reply RADIUS code 3

(18) eap_peap:   Reply-Message := "Please enter OTP"

(18) eap_peap:   EAP-Message = 0x04080004

(18) eap_peap:   Message-Authenticator = 0x00000000000000000000000000000000

(18) eap_peap: Tunneled authentication was rejected

(18) eap_peap: FAILURE

(18) eap: Sending EAP Request (code 1) ID 9 length 46

(18) eap: EAP session adding &reply:State = 0xaa5507c9a25c1ecd

(18)     [eap] = handled

(18)   } # authenticate = handled

(18) Using Post-Auth-Type Challenge

(18) Post-Auth-Type sub-section not found.  Ignoring.

(18) # Executing group from file /etc/raddb/sites-enabled/default

(18) session-state: Saving cached attributes

(18)   Module-Failure-Message := "eap: Failed continuing EAP GTC (6)
session.  EAP sub-module failed"

(18) Sent Access-Challenge Id 8 from 151.143.230.42:1812 to
151.143.16.5:48021 length 0

(18)   EAP-Message =
0x0109002e19001703030023c75c4bff687aa6d3f1bdb96e186b084df07da99dd0cc0c16b678
094cb04ef7d6fee216

(18)   Message-Authenticator = 0x00000000000000000000000000000000

(18)   State = 0xaa5507c9a25c1ecdfce0263177f07a1c

(18) Finished request

 

-
List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
| Threaded
Open this post in threaded view
|

Re: How to send a challenge request via PEAP-GTC

Alan DeKok-2
On Sep 11, 2019, at 1:53 PM, <[hidden email]> <[hidden email]> wrote:
>
> Is it possible to send a challenge response to a user asking them to enter a
> OPT (One Time Password) token using PEAP with GTC?

  Read raddb/mods-available/eap.  There's a "gtc" subsection.  Which contains a "challenge" parameter.

  This is documented.

>  I have followed the
> documentation example and got this working with PAP, but our security team
> will not allow us to use PAP due to security concerns with the week
> encryption used by PAP.  

  Your security team is wrong.  There are no known security issues with the encryption scheme used by PAP.

> The problem I seem to be having is that when I use "challenge" in the
> authenticate section of the inner-tunnel configuration it seems to break the
> tunnel.  When I do this I get the following error message in the debug:
>
> eap: ERROR: Failed continuing EAP GTC (6) session.  EAP sub-module failed.

  Don't invent things.  Read the documentation. and configure the server as documented.

  Alan DeKok.


-
List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
| Threaded
Open this post in threaded view
|

FW: How to send a challenge request via PEAP-GTC

ngoetz24
>On Sep 11, 2019, at 1:53 PM, <[hidden email]> <[hidden email]> wrote:
>>
>> Is it possible to send a challenge response to a user asking them to
enter a
>> OPT (One Time Password) token using PEAP with GTC?
>
> Read raddb/mods-available/eap.  There's a "gtc" subsection.  Which
contains a "challenge" parameter.
>
> This is documented.
>

I have the challenge parameter set, but the user never seems to get prompted
to enter their OTP password.  Not sure if I have it set correctly.  There is
what I have configured:

 

gtc {

                                #  The default challenge, which many clients

                                #  ignore..

                                challenge = "OTP Password: "

 

                                #  The plain-text response which comes back

                                #  is put into a User-Password attribute,

                                #  and passed to another module for

                                #  authentication.  This allows the EAP-GTC

                                #  response to be checked against
plain-text,

                                #  or crypt'd passwords.

                                #

                                #  If you say "Local" instead of "PAP", then

                                #  the module will look for a User-Password

                                #  configured for the request, and do the

                                #  authentication itself.

                                #

                                #auth_type = PAP

                                auth_type = ntlm_auth

                }


>>  I have followed the
>> documentation example and got this working with PAP, but our security
team
>> will not allow us to use PAP due to security concerns with the week
>> encryption used by PAP.  

>  Your security team is wrong.  There are no known security issues with the
encryption scheme used by PAP.

According to our security team, PAP uses a simple xor between the paasowrd
and the hashed value of the shared secret. According to them, this would
make it easy to decrypt the user passwords in intercepted packets.
Regardless if this is true or not, I don't think I will be able to get them
to approve us using PAP. This means I'm stuck using on of the other types.  


>> The problem I seem to be having is that when I use "challenge" in the
>> authenticate section of the inner-tunnel configuration it seems to break
the
>> tunnel.  When I do this I get the following error message in the debug:
>>
>> eap: ERROR: Failed continuing EAP GTC (6) session.  EAP sub-module
failed.
>
>  Don't invent things.  Read the documentation. and configure the server as
documented.
>
>  Alan DeKok.

I am trying to follow the documentation, but I couldn't find any examples of
how to do two factor authentication other then through PAP. I found a few
other posts that other users made who were having similar problems, but I
didn't see any replies where they were able to get it working or how they
did it.  I have read through the documentation contained in the various
config files and am doing my best to try an follow it, but I am having
issues understanding how to do the two-factor authentication through GTC.  I
have the first part of the authentication working where the user sends their
username and password and this gets passed through to ntlm_auth and
authenticated through active directory.  If the credentials are correct, the
user gets logged in without being requested for the second factor.    This
is why I was trying to send a challenge in the authenticate section since
this is how I got it to work with PAP. If I remove this from the config, the
error goes way, but the user gets authenticated without being promted for
the second credential.    This is what my authentication section looks like
with the challenge removed:

authenticate {

                Auth-Type PAP {

                                pap

                }

 

                Auth-Type MSCHAP {

                                mschap

                }

 

                mschap

 

                Auth-Type ntlm_auth {

                                ntlm_auth

                }

                eap

}

 

I'm not sure what I am missing that is preventing the users from getting
prompted for the second factor.

 

-
List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
| Threaded
Open this post in threaded view
|

Re: How to send a challenge request via PEAP-GTC

Alan DeKok-2
On Sep 11, 2019, at 4:11 PM, <[hidden email]> <[hidden email]> wrote:
> I have the challenge parameter set, but the user never seems to get prompted
> to enter their OTP password.  Not sure if I have it set correctly.

  It's pretty simple to configure.

  The issue is likely that the supplicant is unable (or unwilling) to show prompts when using EAP-GTC.  You can't change the supplicant, so you're stuck with however it behaves.

> According to our security team, PAP uses a simple xor between the paasowrd
> and the hashed value of the shared secret.

  If your security people want to educate themselves as to how it *actually* works, they can read RFC 2865 Section 5.2.  It documents the process in detail.

  They're close, but not correct.

> According to them, this would
> make it easy to decrypt the user passwords in intercepted packets.

  Your security people are stupid.

  No one has published an attack on the password encryption mechanism in RADIUS.  If they had, it would be international news.  Every ISP on the planet would be upgrading in a panic.  Every switch / AP manufacturer would be upgrading in a panic.

  Since you haven't seen that, your security people are wrong.

  This isn't rocket science.  Either all of the RADIUS and IETF security people are wrong (and your security people are smarter than everyone else combined), OR the RADIUS and IETF security people are right, and your security people are wrong.

  I've done this for ~25 years.  They haven't.  Amateurs shouldn't have opinions about security.

> Regardless if this is true or not, I don't think I will be able to get them
> to approve us using PAP. This means I'm stuck using on of the other types.  

  Which don't work for other reasons.

  Your security people are *preventing* you from using good security (OTP), because of ignorance about RADIUS security.

> I am trying to follow the documentation, but I couldn't find any examples of
> how to do two factor authentication other then through PAP.

  Because PAP is pretty much all that works.  EAP-GTC works *sometimes*.  But not always.

> I found a few
> other posts that other users made who were having similar problems, but I
> didn't see any replies where they were able to get it working or how they
> did it.  I have read through the documentation contained in the various
> config files and am doing my best to try an follow it, but I am having
> issues understanding how to do the two-factor authentication through GTC.

  If EAP-GTC works, then the user is prompted with the challenge, and enters their password.

  BUT that requires the supplicant to follow the EAP-GTC spec. They often don't.  And, you can't change the supplicant implementation.

> I'm not sure what I am missing that is preventing the users from getting
> prompted for the second factor.

  Nothing.  The supplicant doesn't support it.

  Tell your security people to stop being idiots.  *They* are the ones preventing you from using OTP.  If they complain, tell them the options are:

a) change the EAP-TTLS protocol to allow for this
b) change all of the supplicants on the planet (Google, Microsoft, etc.) to allow for this
c) allow PAP

  Which one is more likely to succeed?  That might be a difficult concept for them to understand. :(

  Alan DeKok.


-
List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
| Threaded
Open this post in threaded view
|

FW: How to send a challenge request via PEAP-GTC

ngoetz24
>On Sep 11, 2019, at 4:11 PM, <[hidden email]> <[hidden email]> wrote:
>> I have the challenge parameter set, but the user never seems to get
prompted
>> to enter their OTP password.  Not sure if I have it set correctly.
>
>  It's pretty simple to configure.
>
>  The issue is likely that the supplicant is unable (or unwilling) to show
prompts when using EAP-GTC.  You can't change the supplicant, so you're
stuck with however it behaves.
>
>> According to our security team, PAP uses a simple xor between the
paasowrd
>> and the hashed value of the shared secret.
>
> If your security people want to educate themselves as to how it *actually*
works, they can read RFC 2865 Section 5.2.  It documents the process in
detail.
>
> They're close, but not correct.
>
>> According to them, this would
>> make it easy to decrypt the user passwords in intercepted packets.
>
> Your security people are stupid.
>
>  No one has published an attack on the password encryption mechanism in
RADIUS.  If they had, it would be international news.  Every ISP on the
planet would be upgrading in a panic.  Every switch / AP manufacturer would
be upgrading in a panic.
>
>  Since you haven't seen that, your security people are wrong.
>
>  This isn't rocket science.  Either all of the RADIUS and IETF security
people are wrong (and your security people are smarter than everyone else
combined), OR the RADIUS and IETF security people are right, and your
security people are wrong.
>
>  I've done this for ~25 years.  They haven't.  Amateurs shouldn't have
opinions about security.
>
>> Regardless if this is true or not, I don't think I will be able to get
them
>> to approve us using PAP. This means I'm stuck using on of the other
types.  
>
>  Which don't work for other reasons.
>
>  Your security people are *preventing* you from using good security (OTP),
because of ignorance about RADIUS security.
>
>> I am trying to follow the documentation, but I couldn't find any examples
of
>> how to do two factor authentication other then through PAP.
>
>  Because PAP is pretty much all that works.  EAP-GTC works *sometimes*.
But not always.
>
>> I found a few
>> other posts that other users made who were having similar problems, but I

>> didn't see any replies where they were able to get it working or how they

>> did it.  I have read through the documentation contained in the various
>> config files and am doing my best to try an follow it, but I am having
>> issues understanding how to do the two-factor authentication through GTC.

>
>  If EAP-GTC works, then the user is prompted with the challenge, and
enters their password.
>
>  BUT that requires the supplicant to follow the EAP-GTC spec. They often
don't.  And, you can't change the supplicant implementation.
>
>> I'm not sure what I am missing that is preventing the users from getting
>> prompted for the second factor.
>
>  Nothing.  The supplicant doesn't support it.



The reason I think the issue is caused by something I misconfigured, and not
a supplicant issue, is because the supplicant seems to be getting an access
accept from RADIUIS which is why it is letting the user in instead of
prompting them for the OTP.  When I look at the debug it seems like it is
setting the OTP Challenge Message first (as configed is eap config), then
pressing the original request through ntlm_auth, and if ntlm_auth succeeds
it sends a access accept back to the supplicant.  Unless I am
misunderstanding something, shouldn't the radius not send an access accept
back to the supplicant unless all the auth conditions are met?  The part I
seem to be missing is where do I configure the radius server to require a
second factor?  In PAP I set this by returning an Access-Challenge in the
authenticate section.  Since this doesn't work the same way in PEAP-GTC, I
seem to be missing some part of the config that tells it to request the
second factor.  Am I just misunderstanding how this works?

 

 

 

-
List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html
| Threaded
Open this post in threaded view
|

Re: How to send a challenge request via PEAP-GTC

Alan DeKok-2
On Sep 11, 2019, at 6:22 PM, [hidden email] wrote:
> The reason I think the issue is caused by something I misconfigured, and not
> a supplicant issue, is because the supplicant seems to be getting an access
> accept from RADIUIS which is why it is letting the user in instead of
> prompting them for the OTP.

  Then that's how the supplicant works.  The behaviour of supplicants is magical and mysterious, unlike FreeRADIUS.

  i.e. supplicants have near-zero debugging output.

>  When I look at the debug it seems like it is
> setting the OTP Challenge Message first (as configed is eap config), then
> pressing the original request through ntlm_auth, and if ntlm_auth succeeds
> it sends a access accept back to the supplicant.  Unless I am
> misunderstanding something, shouldn't the radius not send an access accept
> back to the supplicant unless all the auth conditions are met?

  EAP-GTC provides for a prompt and a reply, it doesn't provide for multiple rounds of challenge-response.

>  The part I
> seem to be missing is where do I configure the radius server to require a
> second factor?  In PAP I set this by returning an Access-Challenge in the
> authenticate section.  Since this doesn't work the same way in PEAP-GTC, I
> seem to be missing some part of the config that tells it to request the
> second factor.  Am I just misunderstanding how this works?

  Nope.  EAP-GTC does challenge / response, but not *multiple* rounds of challenge-response.

  If you want multiple rounds of challenge-response, use PAP.

  Or, do what everyone else does, and tell people to enter a 6-digit OTP followed by their password, all as one field.  FreeRADIUS can then split that into two fields, and check OTP and "real" password separately.

  The issue is that FreeRADIUS can do just about anything, BUT it's limited by (a) the protocols, and (b) the client implementations (supplicant / whatever).  If the protocols and/or supplicants don't support something, then no amount of poking FreeADIUS will make it work.

  i.e. for a conversation to work, *all* parties to the conversation must cooperate.

  Your main solution here is to use PAP.  Again, tell your "security" people that they're incompetent, and that their fears are not based in reality.  Your choices right now are largely;

a) not use OTP
b) use PAP

  A naive and dogmatic approach to security is wrong.  It's done only by the incompetent and/or inexperienced.

  Alan DeKok.


-
List info/subscribe/unsubscribe? See http://www.freeradius.org/list/users.html