Processing LoRaWAN 1.1 join response

Hi! I asked this some time ago but I think it got lost on a topic that was about simulating devices (my bad). Anyway, I’m basically stuck at trying to process a successful join response for LoRaWAN 1.1 (it works with 1.0) using latest version of loraserver and lora-app-server. My Join function looks like this:

//Join sends a join request for a given device (OTAA) and rxInfo.
func (d *Device) Join(client MQTT.Client, gwMac string, rxInfo RxInfo) error {

	d.Joined = false
	devNonceKey := fmt.Sprintf("dev-nonce-%s", d.DevEUI[:])
	var devNonce uint16
	sdn, err := redisClient.Get(devNonceKey).Result()
	if err == nil {
		dn, err := strconv.Atoi(sdn)
		if err == nil {
			devNonce = uint16(dn + 1)
		}
	}
	redisClient.Set(devNonceKey, devNonce, 0)

	d.DevNonce = lorawan.DevNonce(devNonce)

	joinPhy := lorawan.PHYPayload{
		MHDR: lorawan.MHDR{
			MType: lorawan.JoinRequest,
			Major: lorawan.LoRaWANR1,
		},
		MACPayload: &lorawan.JoinRequestPayload{
			JoinEUI:  d.JoinEUI,
			DevEUI:   d.DevEUI,
			DevNonce: d.DevNonce,
		},
	}

	if err := joinPhy.SetUplinkJoinMIC(d.NwkKey); err != nil {
		return err
	}

	joinStr, err := joinPhy.MarshalText()
	if err != nil {
		return err
	}

	message := &Message{
		PhyPayload: string(joinStr),
		RxInfo:     &rxInfo,
	}

	pErr := publish(client, "gateway/"+rxInfo.Mac+"/rx", message)

	return pErr

}

My function to process the join response downlink looks like this:

func (d *Device) processJoinResponse(phy lorawan.PHYPayload, payload []byte, mv lorawan.MACVersion) (string, error) {
	log.Infoln("processing join response")

	err := phy.DecryptJoinAcceptPayload(d.NwkKey)
	if err != nil {
		log.Errorf("can't decrypt join accept: %s", err)
	}

	ok, err := phy.ValidateDownlinkJoinMIC(lorawan.JoinRequestType, d.DevEUI, d.DevNonce, d.NwkKey)
	if err != nil {
		log.Error("failed at join mic function")
		return "", err
	}
	if !ok {
		return "", errors.New("join invalid mic")
	}

	if err := phy.DecryptFOpts(d.NwkKey); err != nil {
		log.Error("failed at opts decryption")
		return "", err
	}

	phyJSON, err := phy.MarshalJSON()
	if err != nil {
		log.Error("failed at json marshal")
		return "", err
	}

	jap := phy.MACPayload.(*lorawan.JoinAcceptPayload)

	log.Debugf("join accept payload: %+v", jap)

	//Check that JoinNonce is greater than the one already stored.
	joinNonceKey := fmt.Sprintf("join-nonce-%s", d.DevEUI[:])
	var joinNonce lorawan.JoinNonce
	sjn, err := redisClient.Get(joinNonceKey).Result()
	if err == nil {
		jn, err := strconv.Atoi(sjn)
		if err == nil {
			joinNonce = lorawan.JoinNonce(jn + 1)
		}
	}

	if jap.JoinNonce <= joinNonce {
		return "", errors.New("got lower or equal JoinNonce from server")
	}

	redisClient.Set(joinNonceKey, joinNonce, 0)

	d.FNwkSIntKey, err = getFNwkSIntKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	if d.MACVersion == 0 {
		d.NwkSEncKey = d.FNwkSIntKey
		d.SNwkSIntKey = d.FNwkSIntKey
	} else {
		d.NwkSEncKey, err = getNwkSEncKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
		d.SNwkSIntKey, err = getSNwkSIntKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	}
	if jap.DLSettings.OptNeg {
		d.AppSKey, err = getAppSKey(jap.DLSettings.OptNeg, d.AppKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	} else {
		d.AppSKey, err = getAppSKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	}
	d.DevAddr = jap.DevAddr
	d.Joined = true
	d.UlFcnt = 0
	d.DlFcnt = 0

	//Set frame counters to 0.
	ulFcntKey := fmt.Sprintf("ul-fcnt-%s", d.DevEUI[:])
	dlFcntKey := fmt.Sprintf("dl-fcnt-%s", d.DevEUI[:])

	redisClient.Set(ulFcntKey, d.UlFcnt, 0)
	redisClient.Set(dlFcntKey, d.DlFcnt, 0)

	return string(phyJSON), nil
}

func (d *Device) processDownlink(phy lorawan.PHYPayload, payload []byte, mv lorawan.MACVersion) (string, error) {

	//Get downlink frame counter and icnrease it by 1.
	dlFcntKey := fmt.Sprintf("dl-fcnt-%s", d.DevEUI[:])
	df, err := redisClient.Get(dlFcntKey).Result()
	if err == nil {
		dfn, err := strconv.Atoi(df)
		if err == nil {
			d.DlFcnt = uint32(dfn)
		}
	}

	ok, err := phy.ValidateDownlinkDataMIC(mv, 0, d.NwkSEncKey)
	if err != nil {
		log.Error("failed at downlink mic function")
		return "", err
	}
	if !ok {
		return "", errors.New("downlink error: invalid mic")
	}

	if err := phy.DecryptFOpts(d.NwkSEncKey); err != nil {
		log.Error("failed at downlink opts decryption")
		return "", err
	}

	if err := phy.DecryptFRMPayload(d.AppSKey); err != nil {
		log.Error("failed at downlink frm payload decryption")
		return "", err
	}

	/*if err := phy.DecodeFOptsToMACCommands(); err != nil {
		log.Error("failed at downlink opts to mac commands decoding")
		return "", err
	}*/

	phyJSON, err := phy.MarshalJSON()
	if err != nil {
		log.Error("failed at downlink json marshal")
		return "", err
	}

	macPayload := phy.MACPayload.(*lorawan.MACPayload)
	log.Infof("mac payload: %+v", macPayload)

	for _, fOpt := range macPayload.FHDR.FOpts {
		opt := fOpt.(*lorawan.MACCommand)
		log.Infof("fOpt: %+v", opt.Payload)
	}

	log.Infof("fctrl: %+v", macPayload.FHDR.FCtrl)

	for _, frmPayload := range macPayload.FRMPayload {
		log.Infof("data payload: %+v", frmPayload.(*lorawan.DataPayload))
	}

	log.Infof("dlFcnt: %d / received Fcnt: %d", d.DlFcnt, macPayload.FHDR.FCnt)

	//Set downlink frame counter.
	d.DlFcnt++
	redisClient.Set(dlFcntKey, d.DlFcnt, 0)
	return string(phyJSON), nil
}

When sending a JoinRequest, everything goes OK at the network’s side and I can see a successful JoinAccept being generated and pushed down:

{
        "downlinkMetaData": {
            "txInfo": {
                "gatewayId": "b827ebfffe9448d0",
                "immediately": false,
                "timeSinceGpsEpoch": null,
                "timestamp": 1558184016,
                "frequency": 923300000,
                "power": 27,
                "modulation": "LORA",
                "loraModulationInfo": {
                    "bandwidth": 500,
                    "spreadingFactor": 10,
                    "codeRate": "4/5",
                    "polarizationInversion": true
                },
                "board": 0,
                "antenna": 0
            }
        },
        "phyPayload": {
            "mhdr": {
                "mType": "JoinAccept",
                "major": "LoRaWANR1"
            },
            "macPayload": {
                "bytes": "xcykrGR0bye6NUmaZHWl10fR7F/NWKJ3BiPl1A=="
            },
            "mic": "51b6018d"
        }
    },
    {
        "uplinkMetaData": {
            "rxInfo": [
                {
                    "gatewayId": "b827ebfffe9448d0",
                    "time": "2019-03-21T16:00:16Z",
                    "timeSinceGpsEpoch": null,
                    "timestamp": 1553184016,
                    "rssi": -57,
                    "loraSnr": 7,
                    "channel": 0,
                    "rfChain": 1,
                    "board": 0,
                    "antenna": 0,
                    "location": {
                        "latitude": -33.4348397,
                        "longitude": -70.6161374,
                        "altitude": 0,
                        "source": "UNKNOWN",
                        "accuracy": 0
                    },
                    "fineTimestampType": "NONE"
                }
            ],
            "txInfo": {
                "frequency": 916800000,
                "modulation": "LORA",
                "loRaModulationInfo": {
                    "bandwidth": 125,
                    "spreadingFactor": 10,
                    "codeRate": "4/5",
                    "polarizationInversion": false
                }
            }
        },
        "phyPayload": {
            "mhdr": {
                "mType": "JoinRequest",
                "major": "LoRaWANR1"
            },
            "macPayload": {
                "joinEUI": "0000000000000003",
                "devEUI": "0000000000000003",
                "devNonce": 12
            },
            "mic": "026ad8c4"
        }
    }

But, at processing the response it’ll fail with downlink error: invalid mic on the MIC validation bits.

Does anyone have any idea what’s wrong with my processing function (maybe some step is out of order, maybe I’m using wrong keys, etc.)?

You can check the whole code at https://github.com/iegomez/lds.

For what is worth, I fixed the MIC validation problem (it needed to be validated with the jsIntKey it seems) and now my function looks like this, but the keys are still incorrectly derived and thus I get a mic error on uplink:

func (d *Device) processJoinResponse(phy lorawan.PHYPayload, payload []byte, mv lorawan.MACVersion) (string, error) {
	log.Infoln("processing join response")

	err := phy.DecryptJoinAcceptPayload(d.NwkKey)
	if err != nil {
		log.Errorf("can't decrypt join accept: %s", err)
	}

	jap := phy.MACPayload.(*lorawan.JoinAcceptPayload)

	if jap.DLSettings.OptNeg {
		jsIntKey, err := getJSIntKey(d.NwkKey, d.DevEUI)
		if err != nil {
			return "", err
		}
		ok, err := phy.ValidateDownlinkJoinMIC(0xFF, d.JoinEUI, d.DevNonce, jsIntKey)
		if err != nil {
			return "", err
		}
		if !ok {
			return "", errors.New("validate downlink join mic not ok")
		}
	} else {
		ok, err := phy.ValidateDownlinkJoinMIC(0xFF, d.JoinEUI, d.DevNonce, d.NwkKey)
		if err != nil {
			return "", err
		}
		if !ok {
			return "", errors.New("validate downlink join mic not ok")
		}
	}

	phyJSON, err := phy.MarshalJSON()
	if err != nil {
		log.Error("failed at json marshal")
		return "", err
	}

	log.Debugf("join accept payload: %+v", jap)

	//Check that JoinNonce is greater than the one already stored.
	joinNonceKey := fmt.Sprintf("join-nonce-%s", d.DevEUI[:])
	var joinNonce lorawan.JoinNonce
	sjn, err := redisClient.Get(joinNonceKey).Result()
	if err == nil {
		jn, err := strconv.Atoi(sjn)
		if err == nil {
			joinNonce = lorawan.JoinNonce(jn)
		}
	}

	if jap.JoinNonce <= joinNonce {
		return "", errors.New("got lower or equal JoinNonce from server")
	}
	d.JoinNonce = jap.JoinNonce
	log.Infof("setting join nonce: %d", d.JoinNonce)
	redisClient.Set(joinNonceKey, uint16(jap.JoinNonce), 0)

	if d.MACVersion == 0 {
		d.FNwkSIntKey, err = getFNwkSIntKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
		d.NwkSEncKey = d.FNwkSIntKey
		d.SNwkSIntKey = d.FNwkSIntKey
	} else {
		d.FNwkSIntKey, err = getFNwkSIntKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
		d.NwkSEncKey, err = getNwkSEncKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
		d.SNwkSIntKey, err = getSNwkSIntKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	}
	if jap.DLSettings.OptNeg {
		d.AppSKey, err = getAppSKey(jap.DLSettings.OptNeg, d.AppKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	} else {
		d.AppSKey, err = getAppSKey(jap.DLSettings.OptNeg, d.NwkKey, jap.HomeNetID, d.DevEUI, jap.JoinNonce, d.DevNonce)
	}

	d.DevAddr = jap.DevAddr
	d.Joined = true
	d.UlFcnt = 0
	d.DlFcnt = 0

	//Set devAddr and keys at redis so we can override those from a file when we were already joined.
	redisFNwksSIntKey := fmt.Sprintf("ul-FNwksSIntKey-%s", d.DevEUI[:])
	redisNwkSEncKey := fmt.Sprintf("ul-NwkSEncKey-%s", d.DevEUI[:])
	redisSNwkSIntKey := fmt.Sprintf("ul-SNwkSIntKey-%s", d.DevEUI[:])
	redisAppSKey := fmt.Sprintf("ul-AppSKey-%s", d.DevEUI[:])
	redisDevAddr := fmt.Sprintf("ul-devAddr-%s", d.DevEUI[:])
	joinKey := fmt.Sprintf("join-%s", d.DevEUI[:])

	redisClient.Set(redisFNwksSIntKey, KeyToHex(d.FNwkSIntKey), 0)
	redisClient.Set(redisNwkSEncKey, KeyToHex(d.NwkSEncKey), 0)
	redisClient.Set(redisSNwkSIntKey, KeyToHex(d.SNwkSIntKey), 0)
	redisClient.Set(redisAppSKey, KeyToHex(d.AppSKey), 0)
	redisClient.Set(redisDevAddr, DevAddressToHex(d.DevAddr), 0)
	redisClient.Set(joinKey, "true", 0)

	//Set frame counters to 0.
	ulFcntKey := fmt.Sprintf("ul-fcnt-%s", d.DevEUI[:])
	dlFcntKey := fmt.Sprintf("dl-fcnt-%s", d.DevEUI[:])

	redisClient.Set(ulFcntKey, d.UlFcnt, 0)
	redisClient.Set(dlFcntKey, d.DlFcnt, 0)

	log.Infoln("Join successful!")

	return string(phyJSON), nil
}

Nevermind, it was a freaking mistype (passing devEUI instead of joinEUI to the key derivation functions). :sweat_smile:

1 Like

Hmm. I only just saw this.

I made a post about something similar happening with a node running Mbed Lorawan 1.1 here:
https://forum.loraserver.io/t/mbed-lorawan-1-1-issues/4032

Could it be the same problem?

To be honest, I don’t know, though I doubt they would make such a silly mistake in the product’s firmware. You also mentioned you checked and keys matched, so it doesn’t seem to be the issue. Unless someone else here has experienced your problem, you’ll probably get better help asking the vendor about it.

Hmm yeah.

It is pre-release and I have since noticed the same issue happens in 1.02 mode, however it takes a few normal messages before the issue appears, whereas 1.1 mode demonstrates the issue right after joining. Their stable stack which only supports 1.02 works fine.

I figure it is down to a problem with their stack. I guess I was just asking, because I didn’t see much activity in their git repo and they don’t make a lot of mention of existing issues.