Documentation
¶
Overview ¶
Package crypto implements the cryptographic primitives used by the RADIUS protocol:
- Response Authenticator (RFC 2865 §3): MD5 over Code+ID+Length+RequestAuth+Attributes+Secret
- Accounting-Request Authenticator (RFC 2866 §3): MD5 over Code+ID+Length+16 zeros+Attributes+Secret
- User-Password hiding (RFC 2865 §5.2): chained MD5 + XOR
- Message-Authenticator (RFC 2869 §5.14): HMAC-MD5 over the entire packet
All functions are pure: callers pass the shared secret explicitly, and no state is retained. Constant-time comparison is used for verification paths.
Index ¶
- Constants
- func ChallengeHash(peerChallenge, authenticatorChallenge [MSCHAPv2ChallengeLength]byte, ...) [ChallengeHashLength]byte
- func ComputeAccountingRequestAuthenticator(code byte, id byte, length uint16, attributes []byte, secret []byte) [16]byte
- func ComputeCHAPResponse(chapID byte, password string, challenge []byte) []byte
- func ComputeMessageAuthenticator(packetBytes []byte, secret []byte) [16]byte
- func ComputeResponseAuthenticator(code byte, id byte, length uint16, requestAuth [16]byte, attributes []byte, ...) [16]byte
- func DecodeTunnelInteger(value []byte) (tag byte, hasTag bool, n uint32, err error)
- func DecodeTunnelTag(value []byte) (tag byte, hasTag bool, payload []byte)
- func DecryptMPPEKey(value []byte, requestAuth [16]byte, secret []byte) ([]byte, error)
- func DecryptTunnelPassword(value []byte, requestAuth [16]byte, secret []byte) ([]byte, error)
- func DecryptUserPassword(ciphertext []byte, requestAuth [16]byte, secret []byte) ([]byte, error)
- func DeriveMPPEKeysFromPassword(password string, ntResponse [NTResponseLength]byte, sessionKeyLength int) (send, recv []byte, err error)
- func EncodeTunnelInteger(tag byte, n uint32) []byte
- func EncodeTunnelTag(tag byte) (byte, bool)
- func EncryptMPPEKey(key []byte, requestAuth [16]byte, secret []byte) ([]byte, error)
- func EncryptTunnelPassword(password []byte, requestAuth [16]byte, secret []byte) ([]byte, error)
- func EncryptUserPassword(password []byte, requestAuth [16]byte, secret []byte) ([]byte, error)
- func EqualConstantTime(a, b []byte) bool
- func GenerateAuthenticatorResponse(authenticatorChallenge, peerChallenge [MSCHAPv2ChallengeLength]byte, ...) string
- func GenerateNTResponse(authenticatorChallenge, peerChallenge [MSCHAPv2ChallengeLength]byte, ...) [NTResponseLength]byte
- func GetAsymmetricStartKey(masterKey [16]byte, sessionKeyLength int, isSend bool) ([]byte, error)
- func GetMasterKey(passwordHashHash [NTHashHashLength]byte, ntResponse [NTResponseLength]byte) [16]byte
- func HashNtPasswordHash(ntHash [NTHashLength]byte) [NTHashHashLength]byte
- func NtPasswordHash(password string) [NTHashLength]byte
- func VerifyCHAPResponse(attr []byte, password string, challenge []byte) bool
- func VerifyMessageAuthenticator(packetBytes []byte, received [16]byte, secret []byte) bool
Constants ¶
const ( // MPPEKeyLength40Bit produces an 8-byte session key. The 40 in the // name refers to the usable key bits: the remaining 24 bits are // fixed and known, so 40-bit MPPE is weak and only used for legacy // compatibility. MPPEKeyLength40Bit = 8 // MPPEKeyLength128Bit produces a 16-byte session key, the strongest // mode supported by MPPE. MPPEKeyLength128Bit = 16 )
MPPE key lengths supported by GetAsymmetricStartKey (RFC 3079 §3.3).
const ( // NTHashLength is the length of an NT password hash: MD4 over the // UTF-16LE encoding of the password (RFC 2759 §8.3). NTHashLength = 16 // NTHashHashLength is the length of the MD4 over the NT hash // (RFC 2759 §8.4): another 16 bytes. NTHashHashLength = 16 // NTResponseLength is the length of the MS-CHAPv2 NT-Response field // (RFC 2759 §8.7): 24 bytes. NTResponseLength = 24 // MSCHAPv2ChallengeLength is the length of the Authenticator // Challenge or Peer Challenge used by MS-CHAPv2 (RFC 2759 §8.6). MSCHAPv2ChallengeLength = 16 // ChallengeHashLength is the 8-byte output of ChallengeHash // (RFC 2759 §8.6). ChallengeHashLength = 8 // AuthenticatorResponseLength is the length of the S=... response // string produced by GenerateAuthenticatorResponse (RFC 2759 §8.7). AuthenticatorResponseLength = 42 )
MS-CHAPv2 / MS-CHAPv1 constants from RFC 2759 and RFC 3079.
const CHAPPasswordLength = 17
CHAPPasswordLength is the wire length of a CHAP-Password attribute value: 1 byte CHAP-ID plus 16 bytes MD5 hash (RFC 2865 §5.3).
Variables ¶
This section is empty.
Functions ¶
func ChallengeHash ¶
func ChallengeHash(peerChallenge, authenticatorChallenge [MSCHAPv2ChallengeLength]byte, userName string) [ChallengeHashLength]byte
ChallengeHash implements RFC 2759 §8.6:
SHA1(peerChallenge + authenticatorChallenge + userName)[:8]
userName is the raw byte form supplied by the caller; the RFC specifies the user name with no domain prefix when a "DOMAIN\user" form is in use. Callers should strip the domain before calling this function.
func ComputeAccountingRequestAuthenticator ¶
func ComputeAccountingRequestAuthenticator(code byte, id byte, length uint16, attributes []byte, secret []byte) [16]byte
ComputeAccountingRequestAuthenticator calculates the Request Authenticator for an Accounting-Request packet (RFC 2866 §3).
Unlike Access-Request (which uses a random value), the Accounting-Request authenticator is itself an MD5 digest because there is no User-Password attribute to hide:
MD5(Code + ID + Length + 16 zero octets + Attributes + Secret)
func ComputeCHAPResponse ¶
ComputeCHAPResponse computes the CHAP-Password attribute value for the given CHAP-ID, password, and challenge, per RFC 2865 §5.3:
MD5(chapID || password || challenge)
chapID is a single byte chosen by the client (typically random). The challenge is normally the 16-byte Request Authenticator of the Access-Request packet, but RFC 2865 §5.40 allows the server to supply a longer challenge via the CHAP-Challenge attribute (type 60) instead — this function accepts an arbitrary-length challenge for that reason.
The returned slice is 17 bytes: chapID followed by the 16-byte digest. It is a fresh allocation; callers may retain it without copying.
func ComputeMessageAuthenticator ¶
ComputeMessageAuthenticator calculates the HMAC-MD5 Message-Authenticator (RFC 2869 §5.14) over an entire RADIUS packet.
The caller MUST zero the 16-octet Value field of the Message-Authenticator attribute within packetBytes before calling this function. The returned digest is written into that field by the packet codec.
The shared secret is used as the HMAC key.
func ComputeResponseAuthenticator ¶
func ComputeResponseAuthenticator(code byte, id byte, length uint16, requestAuth [16]byte, attributes []byte, secret []byte) [16]byte
ComputeResponseAuthenticator calculates the Response Authenticator used by Access-Accept, Access-Reject, Access-Challenge (RFC 2865 §3), and Accounting-Response (RFC 2866 §3).
Formula: MD5(Code + ID + Length + RequestAuth + Attributes + Secret) where Length is big-endian uint16 and RequestAuth is the 16-byte Request Authenticator from the corresponding request packet.
func DecodeTunnelInteger ¶
DecodeTunnelInteger decodes a tagged integer tunnel attribute. Returns the tag (or 0 with hasTag=false when no tag prefix is present) and the integer value.
Ambiguity note: RFC 2868 §3.1 lets integer tunnel attributes omit the tag. When the first byte of the value falls in 0x01..0x1F the decoder cannot tell whether it is a tag or the high byte of the integer. Callers that know from context whether a tag is present should validate accordingly; this helper follows the spec's literal "0x01..0x1F means tag" rule.
func DecodeTunnelTag ¶
DecodeTunnelTag returns the tag and the remaining value bytes from a tagged tunnel attribute Value. If the first byte is in 0x00..0x1F it is treated as a tag and the rest is the payload; otherwise the entire Value is the payload and hasTag=false.
Per RFC 2868 §3.1 the tag field is optional: a first byte in 0x20..0xFF is the start of the actual data, not a tag.
func DecryptMPPEKey ¶ added in v2.4.0
DecryptMPPEKey reverses EncryptMPPEKey. value is the MS-MPPE-Send-Key / MS-MPPE-Recv-Key attribute Value (Salt + encrypted String). requestAuth is the Request Authenticator of the enclosing Access-Request. Returns the key scoped by the Key-Length field; any padding is discarded.
Per RFC 2548 §3.3 Implementation Notes, the returned key may be longer than the encryption scheme in use requires; callers are responsible for any truncation.
func DecryptTunnelPassword ¶
DecryptTunnelPassword reverses EncryptTunnelPassword. value is the Tunnel-Password attribute Value (Salt + encrypted String). requestAuth is the Request Authenticator of the enclosing Access-Request.
Returns the plaintext password recovered via the Data-Length field; any padding is discarded.
func DecryptUserPassword ¶
DecryptUserPassword reverses EncryptUserPassword. The ciphertext length must be a non-zero multiple of 16 and at most 128 octets. Trailing NUL padding introduced during encryption is stripped from the returned plaintext.
func DeriveMPPEKeysFromPassword ¶
func DeriveMPPEKeysFromPassword(password string, ntResponse [NTResponseLength]byte, sessionKeyLength int) (send, recv []byte, err error)
DeriveMPPEKeysFromPassword is a convenience wrapper that performs the full MS-CHAPv2 → MPPE derivation chain:
- NtPasswordHash(password)
- HashNtPasswordHash(NtPasswordHash(password))
- GetMasterKey(hashHash, ntResponse)
- GetAsymmetricStartKey(masterKey, sessionKeyLength, isSend)
Returns the server-perspective send and receive session keys (both sessionKeyLength bytes): send encrypts server→client traffic (and goes in MS-MPPE-Send-Key), recv encrypts client→server traffic (and goes in MS-MPPE-Recv-Key). Callers performing only one side can ignore the unneeded return value.
func EncodeTunnelInteger ¶
EncodeTunnelInteger encodes a tagged integer tunnel attribute (Tunnel-Type, Tunnel-Medium-Type, Tunnel-Preference) per RFC 2868 §3.1. The wire layout is: [Tag] + 4-byte big-endian integer. When tag is 0 the prefix is omitted.
func EncodeTunnelTag ¶
EncodeTunnelTag encodes a 1-byte tag prefix for a tagged tunnel attribute per RFC 2868 §3.1. Tags 0x01..0x1F are valid; tag 0x00 means "no tag" and the returned ok=false signals the caller to omit the prefix.
func EncryptMPPEKey ¶ added in v2.4.0
EncryptMPPEKey encrypts an MPPE session key per RFC 2548 §3.3 for transport in an MS-MPPE-Send-Key (Vendor-Type 16) or MS-MPPE-Recv-Key (Vendor-Type 17) attribute. The wire algorithm is identical to Tunnel-Password (RFC 2868 §3.5): salted MD5-feedback over a length-prefixed plaintext.
requestAuth is the 16-byte Request Authenticator of the enclosing Access-Request. Returns the VSA Value (Salt + encrypted String).
func EncryptTunnelPassword ¶
EncryptTunnelPassword encrypts a tunnel password per RFC 2868 §3.5. requestAuth is the 16-byte Request Authenticator from the enclosing Access-Request packet. The optional Tag is NOT applied here; callers that need a tagged Tunnel-Password attribute should prepend the Tag byte via EncodeTunnelTag at the attribute layer.
Returns the Value field (Salt + encrypted String) ready to be placed in a Tunnel-Password attribute.
func EncryptUserPassword ¶
EncryptUserPassword hides a cleartext password using the RFC 2865 §5.2 algorithm: the password is null-padded to a 16-byte boundary, then each 16-octet block is XORed with MD5(secret || previous_ciphertext_block), where the first block uses the Request Authenticator as the previous block.
The result length is a multiple of 16, between 16 and 128 octets inclusive. Returns ErrPasswordTooLong if len(password) > 128, and ErrSecretEmpty if the shared secret is empty.
func EqualConstantTime ¶
EqualConstantTime reports whether a and b are byte-for-byte equal without leaking timing information about the position of the first difference. Returns false when lengths differ.
func GenerateAuthenticatorResponse ¶
func GenerateAuthenticatorResponse(authenticatorChallenge, peerChallenge [MSCHAPv2ChallengeLength]byte, ntResponse [NTResponseLength]byte, userName, password string) string
GenerateAuthenticatorResponse implements RFC 2759 §8.7. It derives the 42-octet "S=<40 hex>" Authenticator Response string that the server compares against the MS-CHAP2-Response attribute's Authenticator Response field.
digest1 = SHA1(NTHashHash || NTResponse || Magic1) challenge = ChallengeHash(PeerChallenge, AuthenticatorChallenge, UserName) final = SHA1(digest1 || challenge || Magic2) AuthenticatorResponse = "S=" + uppercase_hex(final)
Magic1 (39 octets, "Magic server to client signing constant") selects the password-hash-hash digest; Magic2 (41 octets, "Pad to make it do more than one iteration") binds the 8-byte challenge. This matches FreeRADIUS mschap_auth_response() in src/modules/rlm_mschap/mschap.c.
func GenerateNTResponse ¶
func GenerateNTResponse(authenticatorChallenge, peerChallenge [MSCHAPv2ChallengeLength]byte, userName, password string) [NTResponseLength]byte
GenerateNTResponse implements RFC 2759 §8.7:
DesEncrypt(7 copies of NtPasswordHash[0..7] + Z1, ZPasswordHash[0..7]) DesEncrypt(...) ... 3 blocks ... Concatenated: 24-byte NT-Response
Specifically:
ZPasswordHash = NtPasswordHash(password)
ZPasswordHashHash = HashNtPasswordHash(ZPasswordHash)
Challenge = ChallengeHash(peerChallenge, authChallenge, userName)
NTResponse = Concat(DesEncrypt(ZPasswordHash[0..7] repeated to 8 bytes,
Challenge[0..7]), DesEncrypt(ZPasswordHash[0..7], Challenge[8..15]),
DesEncrypt(ZPasswordHash[0..7], Challenge[0..7] (truncated)))
The three DES keys are derived by repeating the 7-byte NT hash to fill 8 bytes (with bit-reversal parity applied by the DES implementation itself, which we replicate here because Go's crypto/des requires 8-byte keys with valid parity bits — they are ignored in operation but must be present).
func GetAsymmetricStartKey ¶
GetAsymmetricStartKey derives a per-direction session key from the MPPE master key (RFC 3079 §3.4):
SHA1(masterKey || pad1 || magic || pad2)[:sessionKeyLength]
isSend is interpreted from the server's perspective, matching the sample call in RFC 3079 §3.3 (IsSend=TRUE, IsServer=TRUE → magic3). Therefore:
- isSend=true → magic3 (server→client direction; the value to place in the MS-MPPE-Send-Key attribute returned by a RADIUS server)
- isSend=false → magic2 (client→server direction; the value to place in the MS-MPPE-Recv-Key attribute returned by a RADIUS server)
sessionKeyLength must be MPPEKeyLength40Bit (8) or MPPEKeyLength128Bit (16); other values return ErrInvalidLength.
func GetMasterKey ¶
func GetMasterKey(passwordHashHash [NTHashHashLength]byte, ntResponse [NTResponseLength]byte) [16]byte
GetMasterKey derives the 16-byte MPPE master key from the password hash hash and the NT-Response of an MS-CHAPv2 exchange (RFC 3079 §3.4):
SHA1(passwordHashHash || ntResponse || magic1)[:16]
The master key is then used to derive per-direction session keys via GetAsymmetricStartKey.
func HashNtPasswordHash ¶
func HashNtPasswordHash(ntHash [NTHashLength]byte) [NTHashHashLength]byte
HashNtPasswordHash returns the MD4 hash of an NT password hash (RFC 2759 §8.4). This is the "password hash hash" used as input to GenerateAuthenticatorResponse and to MPPE key derivation.
func NtPasswordHash ¶
func NtPasswordHash(password string) [NTHashLength]byte
NtPasswordHash returns the NT hash of password: MD4 over the UTF-16LE encoding of password (RFC 2759 §8.3). The password is encoded to UTF-16LE without a BOM; any byte order mark in the input string is treated as part of the password.
Returned as a fixed-size array so callers can use it directly in downstream operations (HashNtPasswordHash, GenerateNTResponse).
func VerifyCHAPResponse ¶
VerifyCHAPResponse reports whether attr matches the expected CHAP-Password value computed from (chapID, password, challenge). attr must be at least CHAPPasswordLength bytes; the first byte is treated as the CHAP-ID.
Comparison is constant-time so callers can use this in authentication paths without leaking information about the expected digest.
func VerifyMessageAuthenticator ¶
VerifyMessageAuthenticator recomputes the HMAC-MD5 over packetBytes and compares it against received in constant time. As with ComputeMessageAuthenticator, the Value field of the Message-Authenticator attribute in packetBytes must be zeroed before calling.
Types ¶
This section is empty.